~andrewjbeach/juju-ci-tools/make-local-patcher

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