1 from .client
import client
8 from subprocess
import (
15 [re
.compile(r
"amd64|x86_64"), "amd64"],
16 [re
.compile(r
"i?[3-9]86"), "i386"],
17 [re
.compile(r
"(arm$)|(armv.*)"), "armhf"],
18 [re
.compile(r
"aarch64"), "arm64"],
19 [re
.compile(r
"ppc64|ppc64el|ppc64le"), "ppc64el"],
20 [re
.compile(r
"s390x?"), "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
163 # Run w/o allocating a pty, so we fail if sudo prompts for a passwd
164 ssh
= self
._get
_ssh
_client
(
167 self
.private_key_path
,
170 stdout
, stderr
= self
._run
_command
(ssh
, "sudo -n true", pty
=False)
171 except paramiko
.ssh_exception
.AuthenticationException
as e
:
179 # if the above fails, run the init script as the authenticated user
181 # Infer the public key
183 public_key_path
= "{}.pub".format(self
.private_key_path
)
185 if not os
.path
.exists(public_key_path
):
186 raise FileNotFoundError(
187 "Public key '{}' doesn't exist.".format(public_key_path
)
190 with
open(public_key_path
, "r") as f
:
191 public_key
= f
.readline()
193 script
= INITIALIZE_UBUNTU_SCRIPT
.format(public_key
)
196 ssh
= self
._get
_ssh
_client
(
199 self
.private_key_path
,
204 ["sudo", "/bin/bash -c " + shlex
.quote(script
)],
207 except paramiko
.ssh_exception
.AuthenticationException
as e
:
214 def _detect_hardware_and_os(self
, ssh
):
215 """Detect the target hardware capabilities and OS series.
217 :param object ssh: The SSHClient
218 :return: str: A raw string containing OS and hardware information.
228 stdout
, stderr
= self
._run
_command
(
230 ["sudo", "/bin/bash -c " + shlex
.quote(DETECTION_SCRIPT
)],
234 lines
= stdout
.split("\n")
235 info
['series'] = lines
[0].strip()
236 info
['arch'] = normalize_arch(lines
[1].strip())
238 memKb
= re
.split(r
'\s+', lines
[2])[1]
240 # Convert megabytes -> kilobytes
241 info
['mem'] = round(int(memKb
) / 1024)
243 # Detect available CPUs
245 for line
in lines
[3:]:
249 if line
.find("physical id") == 0:
250 physical_id
= line
.split(":")[1].strip()
251 elif line
.find("cpu cores") == 0:
252 cores
= line
.split(":")[1].strip()
254 if physical_id
not in recorded
.keys():
255 info
['cpu-cores'] += cores
256 recorded
[physical_id
] = True
260 def provision_machine(self
):
261 """Perform the initial provisioning of the target machine.
263 :return: bool: The client.AddMachineParams
264 :raises: :class:`paramiko.ssh_exception.AuthenticationException`
267 params
= client
.AddMachineParams()
269 if self
._init
_ubuntu
_user
():
272 ssh
= self
._get
_ssh
_client
(
275 self
.private_key_path
278 hw
= self
._detect
_hardware
_and
_os
(ssh
)
279 params
.series
= hw
['series']
280 params
.instance_id
= "manual:{}".format(self
.host
)
281 params
.nonce
= "manual:{}:{}".format(
283 str(uuid
.uuid4()), # a nop for Juju w/manual machines
285 params
.hardware_characteristics
= {
287 'mem': int(hw
['mem']),
288 'cpu-cores': int(hw
['cpu-cores']),
290 params
.addresses
= [{
296 except paramiko
.ssh_exception
.AuthenticationException
as e
:
303 async def install_agent(self
, connection
, nonce
, machine_id
):
305 :param object connection: Connection to Juju API
306 :param str nonce: The nonce machine specification
307 :param str machine_id: The id assigned to the machine
309 :return: bool: If the initialization was successful
312 # The path where the Juju agent should be installed.
313 data_dir
= "/var/lib/juju"
315 # Disabling this prevents `apt-get update` from running initially, so
316 # charms will fail to deploy
317 disable_package_commands
= False
319 client_facade
= client
.ClientFacade
.from_connection(connection
)
320 results
= await client_facade
.ProvisioningScript(
322 disable_package_commands
,
327 self
._run
_configure
_script
(results
.script
)
329 def _run_configure_script(self
, script
):
330 """Run the script to install the Juju agent on the target machine.
332 :param str script: The script returned by the ProvisioningScript API
333 :raises: :class:`paramiko.ssh_exception.AuthenticationException`
337 _
, tmpFile
= tempfile
.mkstemp()
338 with
open(tmpFile
, 'w') as f
:
343 ssh
= self
._get
_ssh
_client
(
346 self
.private_key_path
,
349 # copy the local copy of the script to the remote machine
350 sftp
= paramiko
.SFTPClient
.from_transport(ssh
.get_transport())
356 # run the provisioning script
357 stdout
, stderr
= self
._run
_command
(
359 "sudo /bin/bash {}".format(tmpFile
),
362 except paramiko
.ssh_exception
.AuthenticationException
as e
: