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"],
24 def normalize_arch(rawArch
):
25 """Normalize the architecture string."""
27 if arch
[0].match(rawArch
):
31 DETECTION_SCRIPT
= """#!/bin/bash
33 os_id=$(grep '^ID=' /etc/os-release | tr -d '"' | cut -d= -f2)
34 if [ "$os_id" = 'centos' ]; then
35 os_version=$(grep '^VERSION_ID=' /etc/os-release | tr -d '"' | cut -d= -f2)
36 echo "centos$os_version"
41 grep MemTotal /proc/meminfo
45 INITIALIZE_UBUNTU_SCRIPT
= """set -e
46 (id ubuntu &> /dev/null) || useradd -m ubuntu -s /bin/bash
49 echo 'ubuntu ALL=(ALL) NOPASSWD:ALL' > $temp
50 install -m 0440 $temp /etc/sudoers.d/90-juju-ubuntu
52 su ubuntu -c 'install -D -m 0600 /dev/null ~/.ssh/authorized_keys'
53 export authorized_keys="{}"
54 if [ ! -z "$authorized_keys" ]; then
55 su ubuntu -c 'echo $authorized_keys >> ~/.ssh/authorized_keys'
61 """Provision a manually created machine via SSH."""
66 def __init__(self
, user
, host
, private_key_path
):
69 self
.private_key_path
= private_key_path
71 def _get_ssh_client(self
, host
, user
, key
):
72 """Return a connected Paramiko ssh object.
74 :param str host: The host to connect to.
75 :param str user: The user to connect as.
76 :param str key: The private key to authenticate with.
78 :return: object: A paramiko.SSHClient
79 :raises: :class:`paramiko.ssh_exception.SSHException` if the
83 ssh
= paramiko
.SSHClient()
84 ssh
.set_missing_host_key_policy(paramiko
.AutoAddPolicy())
88 # Read the private key into a paramiko.RSAKey
89 if os
.path
.exists(key
):
90 with
open(key
, 'r') as f
:
91 pkey
= paramiko
.RSAKey
.from_private_key(f
)
93 #######################################################################
94 # There is a bug in some versions of OpenSSH 4.3 (CentOS/RHEL5) where #
95 # the server may not send the SSH_MSG_USERAUTH_BANNER message except #
96 # when responding to an auth_none request. For example, paramiko will #
97 # attempt to use password authentication when a password is set, but #
98 # the server could deny that, instead requesting keyboard-interactive.#
99 # The hack to workaround this is to attempt a reconnect, which will #
100 # receive the right banner, and authentication can proceed. See the #
101 # following for more info: #
102 # https://github.com/paramiko/paramiko/issues/432 #
103 # https://github.com/paramiko/paramiko/pull/438 #
104 #######################################################################
107 ssh
.connect(host
, port
=22, username
=user
, pkey
=pkey
)
108 except paramiko
.ssh_exception
.SSHException
as e
:
109 if 'Error reading SSH protocol banner' == str(e
):
110 # Once more, with feeling
111 ssh
.connect(host
, port
=22, username
=user
, pkey
=pkey
)
113 # Reraise the original exception
118 def _run_command(self
, ssh
, cmd
, pty
=True):
119 """Run a command remotely via SSH.
121 :param object ssh: The SSHClient
122 :param str cmd: The command to execute
123 :param list cmd: The `shlex.split` command to execute
124 :param bool pty: Whether to allocate a pty
126 :return: tuple: The stdout and stderr of the command execution
127 :raises: :class:`CalledProcessError` if the command fails
130 if isinstance(cmd
, str):
131 cmd
= shlex
.split(cmd
)
133 if type(cmd
) is not list:
137 stdin
, stdout
, stderr
= ssh
.exec_command(cmds
, get_pty
=pty
)
138 retcode
= stdout
.channel
.recv_exit_status()
141 output
= stderr
.read().strip()
142 raise CalledProcessError(returncode
=retcode
, cmd
=cmd
,
145 stdout
.read().decode('utf-8').strip(),
146 stderr
.read().decode('utf-8').strip()
149 def _init_ubuntu_user(self
):
150 """Initialize the ubuntu user.
152 :return: bool: If the initialization was successful
153 :raises: :class:`paramiko.ssh_exception.AuthenticationException`
154 if the authentication fails
157 # TODO: Test this on an image without the ubuntu user setup.
159 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(r
'\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
: