983.1.1
by Martin Packman
Add new Remote class for accessing juju machines |
1 |
"""Remote helper class for communicating with juju machines."""
|
2 |
||
3 |
__metaclass__ = type |
|
4 |
||
994.3.1
by Martin Packman
Make Remote an abstract class and implement winrm based version |
5 |
import abc |
983.1.1
by Martin Packman
Add new Remote class for accessing juju machines |
6 |
import logging |
994.4.1
by Martin Packman
Implement copy function for winrm remote |
7 |
import os |
983.1.1
by Martin Packman
Add new Remote class for accessing juju machines |
8 |
import subprocess |
1044.1.1
by Aaron Bentley
Use the deploy_job script everywhere, fix windows weirdness. |
9 |
import sys |
994.4.1
by Martin Packman
Implement copy function for winrm remote |
10 |
import zlib |
983.1.1
by Martin Packman
Add new Remote class for accessing juju machines |
11 |
|
994.3.1
by Martin Packman
Make Remote an abstract class and implement winrm based version |
12 |
import winrm |
13 |
||
14 |
import utility |
|
15 |
||
16 |
||
17 |
def _remote_for_series(series): |
|
18 |
"""Give an appropriate remote class based on machine series."""
|
|
19 |
if series is not None and series.startswith("win"): |
|
20 |
return WinRmRemote |
|
21 |
return SSHRemote |
|
22 |
||
23 |
||
24 |
def remote_from_unit(client, unit, series=None, status=None): |
|
25 |
"""Create remote instance given a juju client and a unit."""
|
|
26 |
if series is None: |
|
27 |
if status is None: |
|
28 |
status = client.get_status() |
|
29 |
machine = status.get_unit(unit).get("machine") |
|
30 |
if machine is not None: |
|
31 |
series = status.status["machines"].get(machine, {}).get("series") |
|
32 |
remotecls = _remote_for_series(series) |
|
33 |
return remotecls(client, unit, None, series=series, status=status) |
|
34 |
||
35 |
||
36 |
def remote_from_address(address, series=None): |
|
37 |
"""Create remote instance given an address"""
|
|
38 |
remotecls = _remote_for_series(series) |
|
39 |
return remotecls(None, None, address, series=series) |
|
40 |
||
41 |
||
42 |
class _Remote: |
|
994.4.4
by Martin Packman
Update with extra tests and addresses review comments by sinzui |
43 |
"""_Remote represents a juju machine to access over the network."""
|
983.1.1
by Martin Packman
Add new Remote class for accessing juju machines |
44 |
|
994.3.1
by Martin Packman
Make Remote an abstract class and implement winrm based version |
45 |
__metaclass__ = abc.ABCMeta |
46 |
||
47 |
def __init__(self, client, unit, address, series=None, status=None): |
|
983.1.1
by Martin Packman
Add new Remote class for accessing juju machines |
48 |
if address is None and (client is None or unit is None): |
49 |
raise ValueError("Remote needs either address or client and unit") |
|
50 |
self.client = client |
|
51 |
self.unit = unit |
|
52 |
self.use_juju_ssh = unit is not None |
|
53 |
self.address = address |
|
994.3.1
by Martin Packman
Make Remote an abstract class and implement winrm based version |
54 |
self.series = series |
55 |
self.status = status |
|
983.1.1
by Martin Packman
Add new Remote class for accessing juju machines |
56 |
|
57 |
def __repr__(self): |
|
58 |
params = [] |
|
59 |
if self.client is not None: |
|
60 |
params.append("env=" + repr(self.client.env.environment)) |
|
61 |
if self.unit is not None: |
|
62 |
params.append("unit=" + repr(self.unit)) |
|
63 |
if self.address is not None: |
|
64 |
params.append("addr=" + repr(self.address)) |
|
994.3.1
by Martin Packman
Make Remote an abstract class and implement winrm based version |
65 |
return "<{} {}>".format(self.__class__.__name__, " ".join(params)) |
66 |
||
67 |
@abc.abstractmethod |
|
68 |
def cat(self, filename): |
|
994.3.2
by Martin Packman
Improve code docs as suggested bu sinzui in review |
69 |
"""
|
70 |
Get the contents of filename from the remote machine.
|
|
71 |
||
72 |
Environment variables in the filename will be expanded in a according
|
|
73 |
to platform-specific rules.
|
|
74 |
"""
|
|
994.3.1
by Martin Packman
Make Remote an abstract class and implement winrm based version |
75 |
|
76 |
@abc.abstractmethod |
|
77 |
def copy(self, destination_dir, source_globs): |
|
78 |
"""Copy files from the remote machine."""
|
|
79 |
||
80 |
def is_windows(self): |
|
81 |
"""Returns True if remote machine is running windows."""
|
|
82 |
return self.series and self.series.startswith("win") |
|
83 |
||
994.4.4
by Martin Packman
Update with extra tests and addresses review comments by sinzui |
84 |
def get_address(self): |
85 |
"""Gives the address of the remote machine."""
|
|
86 |
self._ensure_address() |
|
87 |
return self.address |
|
88 |
||
89 |
def update_address(self, address): |
|
90 |
"""Change address of remote machine."""
|
|
91 |
self.address = address |
|
92 |
||
994.3.1
by Martin Packman
Make Remote an abstract class and implement winrm based version |
93 |
def _get_status(self): |
94 |
if self.status is None: |
|
95 |
self.status = self.client.get_status() |
|
96 |
return self.status |
|
97 |
||
98 |
def _ensure_address(self): |
|
99 |
if self.address: |
|
100 |
return
|
|
101 |
if self.client is None: |
|
102 |
raise ValueError("No address or client supplied") |
|
103 |
status = self._get_status() |
|
104 |
unit = status.get_unit(self.unit) |
|
105 |
self.address = unit['public-address'] |
|
106 |
||
107 |
||
108 |
class SSHRemote(_Remote): |
|
994.4.4
by Martin Packman
Update with extra tests and addresses review comments by sinzui |
109 |
"""SSHRemote represents a juju machine to access using ssh."""
|
994.3.1
by Martin Packman
Make Remote an abstract class and implement winrm based version |
110 |
|
111 |
_ssh_opts = [ |
|
112 |
"-o", "User ubuntu", |
|
113 |
"-o", "UserKnownHostsFile /dev/null", |
|
114 |
"-o", "StrictHostKeyChecking no", |
|
115 |
]
|
|
116 |
||
117 |
timeout = "5m" |
|
983.1.1
by Martin Packman
Add new Remote class for accessing juju machines |
118 |
|
119 |
def run(self, command): |
|
120 |
"""Run a command on the remote machine."""
|
|
994.3.1
by Martin Packman
Make Remote an abstract class and implement winrm based version |
121 |
if self.use_juju_ssh: |
983.1.1
by Martin Packman
Add new Remote class for accessing juju machines |
122 |
try: |
123 |
return self.client.get_juju_output("ssh", self.unit, command) |
|
124 |
except subprocess.CalledProcessError as e: |
|
125 |
logging.warning("juju ssh to %r failed: %s", self.unit, e) |
|
126 |
self.use_juju_ssh = False |
|
127 |
self._ensure_address() |
|
128 |
args = ["ssh"] |
|
129 |
args.extend(self._ssh_opts) |
|
130 |
args.extend([self.address, command]) |
|
131 |
return self._run_subprocess(args) |
|
132 |
||
133 |
def copy(self, destination_dir, source_globs): |
|
134 |
"""Copy files from the remote machine."""
|
|
135 |
self._ensure_address() |
|
136 |
args = ["scp", "-C"] |
|
137 |
args.extend(self._ssh_opts) |
|
138 |
args.extend(["{}:{}".format(self.address, f) for f in source_globs]) |
|
139 |
args.append(destination_dir) |
|
140 |
self._run_subprocess(args) |
|
141 |
||
994.3.1
by Martin Packman
Make Remote an abstract class and implement winrm based version |
142 |
def cat(self, filename): |
994.3.2
by Martin Packman
Improve code docs as suggested bu sinzui in review |
143 |
"""
|
144 |
Get the contents of filename from the remote machine.
|
|
145 |
||
146 |
Tildes and environment variables in the form $TMP will be expanded.
|
|
147 |
"""
|
|
994.3.1
by Martin Packman
Make Remote an abstract class and implement winrm based version |
148 |
return self.run("cat " + utility.quote(filename)) |
983.1.1
by Martin Packman
Add new Remote class for accessing juju machines |
149 |
|
150 |
def _run_subprocess(self, command): |
|
1044.1.1
by Aaron Bentley
Use the deploy_job script everywhere, fix windows weirdness. |
151 |
# XXX implement this in a Windows-compatible way
|
152 |
if self.timeout and sys.platform != 'win32': |
|
983.1.1
by Martin Packman
Add new Remote class for accessing juju machines |
153 |
command = ["timeout", self.timeout] + command |
154 |
return subprocess.check_output(command) |
|
994.3.1
by Martin Packman
Make Remote an abstract class and implement winrm based version |
155 |
|
156 |
||
157 |
class _SSLSession(winrm.Session): |
|
158 |
||
159 |
def __init__(self, target, auth, transport="ssl"): |
|
160 |
key, cert = auth |
|
161 |
self.url = self._build_url(target, transport) |
|
162 |
self.protocol = winrm.Protocol(self.url, transport=transport, |
|
163 |
cert_key_pem=key, cert_pem=cert) |
|
164 |
||
165 |
||
994.4.1
by Martin Packman
Implement copy function for winrm remote |
166 |
_ps_copy_script = """\ |
1013.2.1
by Martin Packman
Improve powershell script for gathering logs from windows machines |
167 |
$ErrorActionPreference = "Stop"
|
168 |
||
169 |
function OutputEncodedFile {
|
|
170 |
param([String]$filename, [IO.Stream]$instream)
|
|
171 |
$trans = New-Object Security.Cryptography.ToBase64Transform
|
|
172 |
$out = [Console]::OpenStandardOutput()
|
|
173 |
$bs = New-Object Security.Cryptography.CryptoStream($out, $trans,
|
|
174 |
[Security.Cryptography.CryptoStreamMode]::Write)
|
|
175 |
$zs = New-Object IO.Compression.DeflateStream($bs,
|
|
176 |
[IO.Compression.CompressionMode]::Compress)
|
|
177 |
[Console]::Out.Write($filename + "|")
|
|
178 |
try {
|
|
179 |
$instream.CopyTo($zs)
|
|
180 |
} finally {
|
|
181 |
$zs.close()
|
|
182 |
$bs.close()
|
|
183 |
[Console]::Out.Write("`n")
|
|
184 |
}
|
|
185 |
}
|
|
186 |
||
187 |
function GatherFiles {
|
|
188 |
param([String[]]$patterns)
|
|
189 |
ForEach ($pattern in $patterns) {
|
|
190 |
$path = [Environment]::ExpandEnvironmentVariables($pattern)
|
|
191 |
ForEach ($file in Get-Item -path $path) {
|
|
192 |
try {
|
|
193 |
$in = New-Object IO.FileStream($file, [IO.FileMode]::Open,
|
|
194 |
[IO.FileAccess]::Read, [IO.FileShare]"ReadWrite,Delete")
|
|
195 |
OutputEncodedFile -filename $file.name -instream $in
|
|
196 |
} catch {
|
|
197 |
$utf8 = New-Object Text.UTF8Encoding($False)
|
|
198 |
$errstream = New-Object IO.MemoryStream(
|
|
199 |
$utf8.GetBytes($_.Exception), $False)
|
|
200 |
$errfilename = $file.name + ".copyerror"
|
|
201 |
OutputEncodedFile -filename $errfilename -instream $errstream
|
|
202 |
}
|
|
203 |
}
|
|
204 |
}
|
|
205 |
}
|
|
206 |
||
207 |
try {
|
|
1013.2.2
by Martin Packman
Wrap interpolated powershell argument in array |
208 |
GatherFiles -patterns @(%s) |
1013.2.1
by Martin Packman
Improve powershell script for gathering logs from windows machines |
209 |
} catch {
|
210 |
Write-Error $_.Exception
|
|
211 |
exit 1
|
|
994.4.1
by Martin Packman
Implement copy function for winrm remote |
212 |
}
|
213 |
"""
|
|
214 |
||
215 |
||
994.3.1
by Martin Packman
Make Remote an abstract class and implement winrm based version |
216 |
class WinRmRemote(_Remote): |
994.4.4
by Martin Packman
Update with extra tests and addresses review comments by sinzui |
217 |
"""WinRmRemote represents a juju machine to access using winrm."""
|
994.3.1
by Martin Packman
Make Remote an abstract class and implement winrm based version |
218 |
|
994.4.4
by Martin Packman
Update with extra tests and addresses review comments by sinzui |
219 |
def __init__(self, *args, **kwargs): |
220 |
super(WinRmRemote, self).__init__(*args, **kwargs) |
|
994.3.1
by Martin Packman
Make Remote an abstract class and implement winrm based version |
221 |
self._ensure_address() |
994.4.4
by Martin Packman
Update with extra tests and addresses review comments by sinzui |
222 |
self.certs = utility.get_winrm_certs() |
223 |
self.session = _SSLSession(self.address, self.certs) |
|
224 |
||
225 |
def update_address(self, address): |
|
226 |
"""Change address of remote machine, refreshes the winrm session."""
|
|
227 |
self.address = address |
|
228 |
self.session = _SSLSession(self.address, self.certs) |
|
994.3.1
by Martin Packman
Make Remote an abstract class and implement winrm based version |
229 |
|
230 |
_escape = staticmethod(subprocess.list2cmdline) |
|
231 |
||
232 |
def run_cmd(self, cmd_list): |
|
233 |
"""Run cmd and arguments given as a list returning response object."""
|
|
234 |
if isinstance(cmd_list, basestring): |
|
235 |
raise ValueError("run_cmd requires a list not a string") |
|
994.3.2
by Martin Packman
Improve code docs as suggested bu sinzui in review |
236 |
# pywinrm does not correctly escape arguments, fix up by escaping cmd
|
237 |
# and giving args as a list of a single pre-escaped string.
|
|
994.3.1
by Martin Packman
Make Remote an abstract class and implement winrm based version |
238 |
cmd = self._escape(cmd_list[:1]) |
239 |
args = [self._escape(cmd_list[1:])] |
|
994.4.4
by Martin Packman
Update with extra tests and addresses review comments by sinzui |
240 |
return self.session.run_cmd(cmd, args) |
994.3.1
by Martin Packman
Make Remote an abstract class and implement winrm based version |
241 |
|
242 |
def run_ps(self, script): |
|
243 |
"""Run string of powershell returning response object."""
|
|
994.4.4
by Martin Packman
Update with extra tests and addresses review comments by sinzui |
244 |
return self.session.run_ps(script) |
994.3.1
by Martin Packman
Make Remote an abstract class and implement winrm based version |
245 |
|
246 |
def cat(self, filename): |
|
994.3.2
by Martin Packman
Improve code docs as suggested bu sinzui in review |
247 |
"""
|
248 |
Get the contents of filename from the remote machine.
|
|
249 |
||
250 |
Backslashes will be treated as directory seperators. Environment
|
|
251 |
variables in the form %TMP% will be expanded.
|
|
252 |
"""
|
|
994.4.4
by Martin Packman
Update with extra tests and addresses review comments by sinzui |
253 |
result = self.session.run_cmd("type", [self._escape([filename])]) |
994.3.1
by Martin Packman
Make Remote an abstract class and implement winrm based version |
254 |
if result.status_code: |
255 |
logging.warning("winrm cat failed %r", result) |
|
256 |
return result.std_out |
|
257 |
||
258 |
def copy(self, destination_dir, source_globs): |
|
259 |
"""Copy files from the remote machine."""
|
|
994.4.4
by Martin Packman
Update with extra tests and addresses review comments by sinzui |
260 |
# Encode globs into script to run on remote machine and return result.
|
994.4.1
by Martin Packman
Implement copy function for winrm remote |
261 |
script = _ps_copy_script % ",".join(s.join('""') for s in source_globs) |
262 |
result = self.run_ps(script) |
|
1013.2.1
by Martin Packman
Improve powershell script for gathering logs from windows machines |
263 |
if result.status_code: |
994.4.3
by Martin Packman
Selection of fixes needed for passing job |
264 |
logging.warning("winrm copy stderr:\n%s", result.std_err) |
994.4.2
by Martin Packman
Use winrm copy implementation to get logs from windows machines |
265 |
raise subprocess.CalledProcessError(result.status_code, |
266 |
"powershell", result) |
|
994.4.1
by Martin Packman
Implement copy function for winrm remote |
267 |
self._encoded_copy_to_dir(destination_dir, result.std_out) |
268 |
||
269 |
@staticmethod
|
|
270 |
def _encoded_copy_to_dir(destination_dir, output): |
|
994.4.4
by Martin Packman
Update with extra tests and addresses review comments by sinzui |
271 |
"""Write remote files from powershell script to disk.
|
272 |
||
273 |
The given output from the powershell script is one line per file, with
|
|
274 |
the filename first, then a pipe, then the base64 encoded deflated file
|
|
275 |
contents. This method reverses that process and creates the files in
|
|
276 |
the given destination_dir.
|
|
277 |
"""
|
|
994.4.1
by Martin Packman
Implement copy function for winrm remote |
278 |
start = 0 |
279 |
while True: |
|
280 |
end = output.find("\n", start) |
|
281 |
if end == -1: |
|
282 |
break
|
|
283 |
mid = output.find("|", start, end) |
|
284 |
if mid == -1: |
|
285 |
if not output[start:end].rstrip("\r\n"): |
|
286 |
break
|
|
287 |
raise ValueError("missing filename in encoded copy data") |
|
288 |
filename = output[start:mid] |
|
289 |
if "/" in filename: |
|
994.4.4
by Martin Packman
Update with extra tests and addresses review comments by sinzui |
290 |
# Just defense against path traversal bugs, should never reach.
|
291 |
raise ValueError("path not filename {!r}".format(filename)) |
|
994.4.1
by Martin Packman
Implement copy function for winrm remote |
292 |
with open(os.path.join(destination_dir, filename), "wb") as f: |
293 |
f.write(zlib.decompress(output[mid+1:end].decode("base64"), |
|
294 |
-zlib.MAX_WBITS)) |
|
295 |
start = end + 1 |