sshClientUtility.ts 6.6 KB
Newer Older
Deshui Yu's avatar
Deshui Yu committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
 * Copyright (c) Microsoft Corporation
 * All rights reserved.
 *
 * MIT License
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
 * documentation files (the "Software"), to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
 * to permit persons to whom the Software is furnished to do so, subject to the following conditions:
 * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
 * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

'use strict';

22
import * as assert from 'assert';
Deshui Yu's avatar
Deshui Yu committed
23
24
import * as cpp from 'child-process-promise';
import * as path from 'path';
25
import * as os from 'os';
Deshui Yu's avatar
Deshui Yu committed
26
import { Client, ClientChannel, SFTPWrapper } from 'ssh2';
27
import * as stream from 'stream';
Deshui Yu's avatar
Deshui Yu committed
28
29
import { Deferred } from 'ts-deferred';
import { NNIError, NNIErrorNames } from '../../common/errors';
30
31
import { getLogger } from '../../common/log';
import { uniqueString } from '../../common/utils';
Deshui Yu's avatar
Deshui Yu committed
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import { RemoteCommandResult } from './remoteMachineData';

/**
 *
 * Utility for frequent operations towards SSH client
 *
 */
export namespace SSHClientUtility {
    /**
     * Copy files and directories in local directory recursively to remote directory
     * @param localDirectory local diretory
     * @param remoteDirectory remote directory
     * @param sshClient SSH client
     */
    export async function copyDirectoryToRemote(localDirectory : string, remoteDirectory : string, sshClient : Client) : Promise<void> {
        const deferred: Deferred<void> = new Deferred<void>();
48
49
50
        const tmpTarName: string = `${uniqueString(10)}.tar.gz`;
        const localTarPath: string = path.join(os.tmpdir(), tmpTarName);
        const remoteTarPath: string = path.join(os.tmpdir(), tmpTarName);
Deshui Yu's avatar
Deshui Yu committed
51
52

        // Compress files in local directory to experiment root directory
53
        await cpp.exec(`tar -czf ${localTarPath} -C ${localDirectory} .`);
Deshui Yu's avatar
Deshui Yu committed
54
        // Copy the compressed file to remoteDirectory and delete it
55
56
        await copyFileToRemote(localTarPath, remoteTarPath, sshClient);
        await cpp.exec(`rm ${localTarPath}`);
Deshui Yu's avatar
Deshui Yu committed
57
        // Decompress the remote compressed file in and delete it
58
59
        await remoteExeCommand(`tar -oxzf ${remoteTarPath} -C ${remoteDirectory}`, sshClient);
        await remoteExeCommand(`rm ${remoteTarPath}`, sshClient);
Deshui Yu's avatar
Deshui Yu committed
60
61
62
63
64
65
66
67
68
69
70
        deferred.resolve();

        return deferred.promise;
    }

    /**
     * Copy local file to remote path
     * @param localFilePath the path of local file
     * @param remoteFilePath the target path in remote machine
     * @param sshClient SSH Client
     */
71
72
73
    export function copyFileToRemote(localFilePath : string, remoteFilePath : string, sshClient : Client) : Promise<boolean> {
        assert(sshClient !== undefined);
        const deferred: Deferred<boolean> = new Deferred<boolean>();
Deshui Yu's avatar
Deshui Yu committed
74
75
        sshClient.sftp((err : Error, sftp : SFTPWrapper) => {
            if (err) {
76
77
78
79
                getLogger().error(`copyFileToRemote: ${err.message}, ${localFilePath}, ${remoteFilePath}`);
                deferred.reject(err);

                return;
Deshui Yu's avatar
Deshui Yu committed
80
            }
81
            assert(sftp !== undefined);
Deshui Yu's avatar
Deshui Yu committed
82
83
84
            sftp.fastPut(localFilePath, remoteFilePath, (fastPutErr : Error) => {
                sftp.end();
                if (fastPutErr) {
85
                    deferred.reject(fastPutErr);
Deshui Yu's avatar
Deshui Yu committed
86
                } else {
87
                    deferred.resolve(true);
Deshui Yu's avatar
Deshui Yu committed
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
                }
            });
        });

        return deferred.promise;
    }

    /**
     * Execute command on remote machine
     * @param command the command to execute remotely
     * @param client SSH Client
     */
    export function remoteExeCommand(command : string, client : Client): Promise<RemoteCommandResult> {
        const deferred : Deferred<RemoteCommandResult> = new Deferred<RemoteCommandResult>();
        let stdout: string = '';
        let stderr: string = '';
        let exitCode : number;

        client.exec(command, (err : Error, channel : ClientChannel) => {
            if (err) {
108
                getLogger().error(`remoteExeCommand: ${err.message}`);
Deshui Yu's avatar
Deshui Yu committed
109
                deferred.reject(err);
110
111

                return;
Deshui Yu's avatar
Deshui Yu committed
112
113
            }

114
            channel.on('data', (data : any, dataStderr : any) => {
Deshui Yu's avatar
Deshui Yu committed
115
116
                if (dataStderr) {
                    stderr += data.toString();
117
                } else {
Deshui Yu's avatar
Deshui Yu committed
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
                    stdout += data.toString();
                }
            }).on('exit', (code, signal) => {
                exitCode = code as number;
                deferred.resolve({
                    stdout : stdout,
                    stderr : stderr,
                    exitCode : exitCode
                });
            });
        });

        return deferred.promise;
    }

    export function getRemoteFileContent(filePath: string, sshClient: Client): Promise<string> {
        const deferred: Deferred<string> = new Deferred<string>();
        sshClient.sftp((err: Error, sftp : SFTPWrapper) => {
            if (err) {
137
                getLogger().error(`getRemoteFileContent: ${err.message}`);
Deshui Yu's avatar
Deshui Yu committed
138
                deferred.reject(new Error(`SFTP error: ${err.message}`));
139
140

                return;
Deshui Yu's avatar
Deshui Yu committed
141
142
143
144
145
146
147
148
            }
            try {
                const sftpStream : stream.Readable = sftp.createReadStream(filePath);

                let dataBuffer: string = '';
                sftpStream.on('data', (data : Buffer | string) => {
                    dataBuffer += data;
                }).on('error', (streamErr: Error) => {
149
                    sftp.end();
Deshui Yu's avatar
Deshui Yu committed
150
151
                    deferred.reject(new NNIError(NNIErrorNames.NOT_FOUND, streamErr.message));
                }).on('end', () => {
152
153
                    // sftp connection need to be released manually once operation is done
                    sftp.end();
Deshui Yu's avatar
Deshui Yu committed
154
155
156
                    deferred.resolve(dataBuffer);
                });
            } catch (error) {
157
158
                getLogger().error(`getRemoteFileContent: ${error.message}`);
                sftp.end();
Deshui Yu's avatar
Deshui Yu committed
159
160
161
162
163
164
165
                deferred.reject(new Error(`SFTP error: ${error.message}`));
            }
        });

        return deferred.promise;
    }
}