1 from .client
import client
8 from subprocess
import (
15 [re
.compile("amd64|x86_64"), "amd64"],
16 [re
.compile("i?[3-9]86"), "i386"],
17 [re
.compile("(arm$)|(armv.*)"), "armhf"],
18 [re
.compile("aarch64"), "arm64"],
19 [re
.compile("ppc64|ppc64el|ppc64le"), "ppc64el"],
20 [re
.compile("ppc64|ppc64el|ppc64le"), "s390x"],
25 def normalize_arch(rawArch
):
26 """Normalize the architecture string."""
28 if arch
[0].match(rawArch
):
32 DETECTION_SCRIPT
= """#!/bin/bash
34 os_id=$(grep '^ID=' /etc/os-release | tr -d '"' | cut -d= -f2)
35 if [ "$os_id" = 'centos' ]; then
36 os_version=$(grep '^VERSION_ID=' /etc/os-release | tr -d '"' | cut -d= -f2)
37 echo "centos$os_version"
42 grep MemTotal /proc/meminfo
46 INITIALIZE_UBUNTU_SCRIPT
= """set -e
47 (id ubuntu &> /dev/null) || useradd -m ubuntu -s /bin/bash
50 echo 'ubuntu ALL=(ALL) NOPASSWD:ALL' > $temp
51 install -m 0440 $temp /etc/sudoers.d/90-juju-ubuntu
53 su ubuntu -c 'install -D -m 0600 /dev/null ~/.ssh/authorized_keys'
54 export authorized_keys="{}"
55 if [ ! -z "$authorized_keys" ]; then
56 su ubuntu -c 'echo $authorized_keys >> ~/.ssh/authorized_keys'
62 """Provision a manually created machine via SSH."""
67 def __init__(self
, user
, host
, private_key_path
):
70 self
.private_key_path
= private_key_path
72 def _get_ssh_client(self
, host
, user
, key
):
73 """Return a connected Paramiko ssh object.
75 :param str host: The host to connect to.
76 :param str user: The user to connect as.
77 :param str key: The private key to authenticate with.
79 :return: object: A paramiko.SSHClient
80 :raises: :class:`paramiko.ssh_exception.SSHException` if the
84 ssh
= paramiko
.SSHClient()
85 ssh
.set_missing_host_key_policy(paramiko
.AutoAddPolicy())
89 # Read the private key into a paramiko.RSAKey
90 if os
.path
.exists(key
):
91 with
open(key
, 'r') as f
:
92 pkey
= paramiko
.RSAKey
.from_private_key(f
)
94 #######################################################################
95 # There is a bug in some versions of OpenSSH 4.3 (CentOS/RHEL5) where #
96 # the server may not send the SSH_MSG_USERAUTH_BANNER message except #
97 # when responding to an auth_none request. For example, paramiko will #
98 # attempt to use password authentication when a password is set, but #
99 # the server could deny that, instead requesting keyboard-interactive.#
100 # The hack to workaround this is to attempt a reconnect, which will #
101 # receive the right banner, and authentication can proceed. See the #
102 # following for more info: #
103 # https://github.com/paramiko/paramiko/issues/432 #
104 # https://github.com/paramiko/paramiko/pull/438 #
105 #######################################################################
108 ssh
.connect(host
, port
=22, username
=user
, pkey
=pkey
)
109 except paramiko
.ssh_exception
.SSHException
as e
:
110 if 'Error reading SSH protocol banner' == str(e
):
111 # Once more, with feeling
112 ssh
.connect(host
, port
=22, username
=user
, pkey
=pkey
)
114 # Reraise the original exception
119 def _run_command(self
, ssh
, cmd
, pty
=True):
120 """Run a command remotely via SSH.
122 :param object ssh: The SSHClient
123 :param str cmd: The command to execute
124 :param list cmd: The `shlex.split` command to execute
125 :param bool pty: Whether to allocate a pty
127 :return: tuple: The stdout and stderr of the command execution
128 :raises: :class:`CalledProcessError` if the command fails
131 if isinstance(cmd
, str):
132 cmd
= shlex
.split(cmd
)
134 if type(cmd
) is not list:
138 stdin
, stdout
, stderr
= ssh
.exec_command(cmds
, get_pty
=pty
)
139 retcode
= stdout
.channel
.recv_exit_status()
142 output
= stderr
.read().strip()
143 raise CalledProcessError(returncode
=retcode
, cmd
=cmd
,
146 stdout
.read().decode('utf-8').strip(),
147 stderr
.read().decode('utf-8').strip()
150 def _init_ubuntu_user(self
):
151 """Initialize the ubuntu user.
153 :return: bool: If the initialization was successful
154 :raises: :class:`paramiko.ssh_exception.AuthenticationException`
155 if the authentication fails
158 # TODO: Test this on an image without the ubuntu user setup.
160 auth_user
= self
.user
162 # Run w/o allocating a pty, so we fail if sudo prompts for a passwd
163 ssh
= self
._get
_ssh
_client
(
166 self
.private_key_path
,
169 stdout
, stderr
= self
._run
_command
(ssh
, "sudo -n true", pty
=False)
170 except paramiko
.ssh_exception
.AuthenticationException
as e
:
178 # if the above fails, run the init script as the authenticated user
180 # Infer the public key
182 public_key_path
= "{}.pub".format(self
.private_key_path
)
184 if not os
.path
.exists(public_key_path
):
185 raise FileNotFoundError(
186 "Public key '{}' doesn't exist.".format(public_key_path
)
189 with
open(public_key_path
, "r") as f
:
190 public_key
= f
.readline()
192 script
= INITIALIZE_UBUNTU_SCRIPT
.format(public_key
)
195 ssh
= self
._get
_ssh
_client
(
198 self
.private_key_path
,
203 ["sudo", "/bin/bash -c " + shlex
.quote(script
)],
206 except paramiko
.ssh_exception
.AuthenticationException
as e
:
213 def _detect_hardware_and_os(self
, ssh
):
214 """Detect the target hardware capabilities and OS series.
216 :param object ssh: The SSHClient
217 :return: str: A raw string containing OS and hardware information.
227 stdout
, stderr
= self
._run
_command
(
229 ["sudo", "/bin/bash -c " + shlex
.quote(DETECTION_SCRIPT
)],
233 lines
= stdout
.split("\n")
234 info
['series'] = lines
[0].strip()
235 info
['arch'] = normalize_arch(lines
[1].strip())
237 memKb
= re
.split('\s+', lines
[2])[1]
239 # Convert megabytes -> kilobytes
240 info
['mem'] = round(int(memKb
) / 1024)
242 # Detect available CPUs
244 for line
in lines
[3:]:
248 if line
.find("physical id") == 0:
249 physical_id
= line
.split(":")[1].strip()
250 elif line
.find("cpu cores") == 0:
251 cores
= line
.split(":")[1].strip()
253 if physical_id
not in recorded
.keys():
254 info
['cpu-cores'] += cores
255 recorded
[physical_id
] = True
259 def provision_machine(self
):
260 """Perform the initial provisioning of the target machine.
262 :return: bool: The client.AddMachineParams
263 :raises: :class:`paramiko.ssh_exception.AuthenticationException`
266 params
= client
.AddMachineParams()
268 if self
._init
_ubuntu
_user
():
271 ssh
= self
._get
_ssh
_client
(
274 self
.private_key_path
277 hw
= self
._detect
_hardware
_and
_os
(ssh
)
278 params
.series
= hw
['series']
279 params
.instance_id
= "manual:{}".format(self
.host
)
280 params
.nonce
= "manual:{}:{}".format(
282 str(uuid
.uuid4()), # a nop for Juju w/manual machines
284 params
.hardware_characteristics
= {
286 'mem': int(hw
['mem']),
287 'cpu-cores': int(hw
['cpu-cores']),
289 params
.addresses
= [{
295 except paramiko
.ssh_exception
.AuthenticationException
as e
:
302 async def install_agent(self
, connection
, nonce
, machine_id
):
304 :param object connection: Connection to Juju API
305 :param str nonce: The nonce machine specification
306 :param str machine_id: The id assigned to the machine
308 :return: bool: If the initialization was successful
311 # The path where the Juju agent should be installed.
312 data_dir
= "/var/lib/juju"
314 # Disabling this prevents `apt-get update` from running initially, so
315 # charms will fail to deploy
316 disable_package_commands
= False
318 client_facade
= client
.ClientFacade
.from_connection(connection
)
319 results
= await client_facade
.ProvisioningScript(
321 disable_package_commands
,
326 self
._run
_configure
_script
(results
.script
)
328 def _run_configure_script(self
, script
):
329 """Run the script to install the Juju agent on the target machine.
331 :param str script: The script returned by the ProvisioningScript API
332 :raises: :class:`paramiko.ssh_exception.AuthenticationException`
336 _
, tmpFile
= tempfile
.mkstemp()
337 with
open(tmpFile
, 'w') as f
:
342 ssh
= self
._get
_ssh
_client
(
345 self
.private_key_path
,
348 # copy the local copy of the script to the remote machine
349 sftp
= paramiko
.SFTPClient
.from_transport(ssh
.get_transport())
355 # run the provisioning script
356 stdout
, stderr
= self
._run
_command
(
358 "sudo /bin/bash {}".format(tmpFile
),
361 except paramiko
.ssh_exception
.AuthenticationException
as e
: