mirror of
https://github.com/flutter/packages.git
synced 2025-07-02 16:39:13 +08:00
Several improvements to fuchsia_ctl. (#134)
* Several improvements to fuchsia_ctl. * Adds timeouts for paving process. * Adds timeouts to ssh commands and tests. * Support to generate authorized_keys with existing pub key. * Add flags to pass public key to pave subcommand. https://github.com/flutter/flutter/issues/54416 * Link CLI flags to command implementations. * Enables command help. * Address code review comments.
This commit is contained in:
@ -39,7 +39,8 @@ Future<void> main(List<String> args) async {
|
||||
'If not specified, the first discoverable device will be used.')
|
||||
..addOption('dev-finder-path',
|
||||
defaultsTo: './dev_finder',
|
||||
help: 'The path to the dev_finder executable.');
|
||||
help: 'The path to the dev_finder executable.')
|
||||
..addFlag('help', defaultsTo: false, help: 'Prints help.');
|
||||
parser.addCommand('ssh')
|
||||
..addFlag('interactive',
|
||||
abbr: 'i',
|
||||
@ -50,8 +51,12 @@ Future<void> main(List<String> args) async {
|
||||
help: 'The command to run on the device. '
|
||||
'If specified, --interactive is ignored.')
|
||||
..addOption('identity-file',
|
||||
defaultsTo: '.ssh/pkey', help: 'The key to use when SSHing.');
|
||||
defaultsTo: '.ssh/pkey', help: 'The key to use when SSHing.')
|
||||
..addOption('timeout-seconds',
|
||||
defaultsTo: '120', help: 'Ssh command timeout in seconds.');
|
||||
parser.addCommand('pave')
|
||||
..addOption('public-key',
|
||||
abbr: 'p', help: 'The public key to add to authorized_keys.')
|
||||
..addOption('image',
|
||||
abbr: 'i', help: 'The system image tgz to unpack and pave.');
|
||||
|
||||
@ -89,7 +94,9 @@ Future<void> main(List<String> args) async {
|
||||
abbr: 'a',
|
||||
help: 'Command line arguments to pass when invoking the tests')
|
||||
..addMultiOption('far',
|
||||
abbr: 'f', help: 'The .far files to include for the test.');
|
||||
abbr: 'f', help: 'The .far files to include for the test.')
|
||||
..addOption('timeout-seconds',
|
||||
defaultsTo: '120', help: 'Test timeout in seconds.');
|
||||
|
||||
final ArgResults results = parser.parse(args);
|
||||
|
||||
@ -98,6 +105,12 @@ Future<void> main(List<String> args) async {
|
||||
stderr.writeln(parser.usage);
|
||||
exit(-1);
|
||||
}
|
||||
|
||||
if (results['help']) {
|
||||
stderr.writeln(parser.commands[results.command.name].usage);
|
||||
exit(0);
|
||||
}
|
||||
|
||||
final AsyncResult command = commands[results.command.name];
|
||||
if (command == null) {
|
||||
stderr.writeln('Unkown command ${results.command.name}.');
|
||||
@ -129,11 +142,11 @@ Future<OperationResult> ssh(
|
||||
identityFilePath: identityFile,
|
||||
);
|
||||
}
|
||||
final OperationResult result = await sshClient.runCommand(
|
||||
targetIp,
|
||||
identityFilePath: identityFile,
|
||||
command: args['command'].split(' '),
|
||||
);
|
||||
final OperationResult result = await sshClient.runCommand(targetIp,
|
||||
identityFilePath: identityFile,
|
||||
command: args['command'].split(' '),
|
||||
timeoutMs:
|
||||
Duration(milliseconds: int.parse(args['timeout-seconds']) * 1000));
|
||||
stdout.writeln(
|
||||
'==================================== STDOUT ====================================');
|
||||
stdout.writeln(result.info);
|
||||
@ -150,7 +163,11 @@ Future<OperationResult> pave(
|
||||
ArgResults args,
|
||||
) async {
|
||||
const ImagePaver paver = ImagePaver();
|
||||
return await paver.pave(args['image'], deviceName);
|
||||
return await paver.pave(
|
||||
args['image'],
|
||||
deviceName,
|
||||
publicKeyPath: args['public-key'],
|
||||
);
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
@ -271,6 +288,8 @@ Future<OperationResult> test(
|
||||
'fuchsia-pkg://fuchsia.com/$target#meta/$target.cmx',
|
||||
arguments
|
||||
],
|
||||
timeoutMs:
|
||||
Duration(milliseconds: int.parse(args['timeout-seconds']) * 1000),
|
||||
);
|
||||
stdout.writeln('Test results (passed: ${testResult.success}):');
|
||||
if (result.info != null) {
|
||||
|
@ -27,17 +27,19 @@ class ImagePaver {
|
||||
this.processManager = const LocalProcessManager(),
|
||||
this.fs = const LocalFileSystem(),
|
||||
this.tar = const SystemTar(processManager: LocalProcessManager()),
|
||||
this.sshKeyManager =
|
||||
const SystemSshKeyManager(processManager: LocalProcessManager()),
|
||||
this.sshKeyManagerProvider = SystemSshKeyManager.defaultProvider,
|
||||
}) : assert(processManager != null),
|
||||
assert(fs != null),
|
||||
assert(tar != null),
|
||||
assert(sshKeyManager != null);
|
||||
assert(tar != null);
|
||||
|
||||
/// The [ProcessManager] used to launch the boot server, `tar`,
|
||||
/// and `ssh-keygen`.
|
||||
final ProcessManager processManager;
|
||||
|
||||
/// The default pave timeout as [Duration] in milliseconds.
|
||||
static const Duration defaultPaveTimeoutMs =
|
||||
Duration(milliseconds: 5 * 60 * 1000);
|
||||
|
||||
/// The [FileSystem] implementation used to
|
||||
final FileSystem fs;
|
||||
|
||||
@ -45,7 +47,7 @@ class ImagePaver {
|
||||
final Tar tar;
|
||||
|
||||
/// The implementation to use for creating SSH keys.
|
||||
final SshKeyManager sshKeyManager;
|
||||
final SshKeyManagerProvider sshKeyManagerProvider;
|
||||
|
||||
/// Paves an image (in .tgz format) to the specified device.
|
||||
///
|
||||
@ -54,7 +56,9 @@ class ImagePaver {
|
||||
Future<OperationResult> pave(
|
||||
String imageTgzPath,
|
||||
String deviceName, {
|
||||
String publicKeyPath,
|
||||
bool verbose = true,
|
||||
Duration timeoutMs = defaultPaveTimeoutMs,
|
||||
}) async {
|
||||
assert(imageTgzPath != null);
|
||||
if (deviceName == null) {
|
||||
@ -62,6 +66,11 @@ class ImagePaver {
|
||||
'If multiple devices are attached, this may result in paving '
|
||||
'an unexpected device.');
|
||||
}
|
||||
final SshKeyManager sshKeyManager = sshKeyManagerProvider(
|
||||
processManager: processManager,
|
||||
publicKeyPath: publicKeyPath,
|
||||
fs: fs,
|
||||
);
|
||||
final String uuid = Uuid().v4();
|
||||
final Directory imageDirectory = fs.directory('image_$uuid');
|
||||
if (verbose) {
|
||||
@ -98,7 +107,7 @@ class ImagePaver {
|
||||
if (deviceName != null) ...<String>['-n', deviceName],
|
||||
'--authorized-keys', '.ssh/authorized_keys',
|
||||
],
|
||||
);
|
||||
).timeout(timeoutMs);
|
||||
final StringBuffer paveStdout = StringBuffer();
|
||||
final StringBuffer paveStderr = StringBuffer();
|
||||
paveProcess.stdout.transform(utf8.decoder).forEach((String s) {
|
||||
|
@ -2,6 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
@ -25,6 +26,10 @@ class SshClient {
|
||||
/// The [ProcessManager] to use for spawning `ssh`.
|
||||
final ProcessManager processManager;
|
||||
|
||||
/// The default ssh timeout as [Duration] in milliseconds.
|
||||
static const Duration defaultSshTimeoutMs =
|
||||
Duration(milliseconds: 5 * 60 * 1000);
|
||||
|
||||
/// Creates a list of arguments to pass to ssh.
|
||||
///
|
||||
/// This method is not intended for use outside of this library, except for
|
||||
@ -84,23 +89,24 @@ class SshClient {
|
||||
/// [DevFinder] class.
|
||||
///
|
||||
/// All arguments must not be null.
|
||||
Future<OperationResult> runCommand(
|
||||
String targetIp, {
|
||||
@required String identityFilePath,
|
||||
@required List<String> command,
|
||||
}) async {
|
||||
Future<OperationResult> runCommand(String targetIp,
|
||||
{@required String identityFilePath,
|
||||
@required List<String> command,
|
||||
Duration timeoutMs = defaultSshTimeoutMs}) async {
|
||||
assert(targetIp != null);
|
||||
assert(identityFilePath != null);
|
||||
assert(command != null);
|
||||
|
||||
return OperationResult.fromProcessResult(
|
||||
await processManager.run(
|
||||
getSshArguments(
|
||||
identityFilePath: identityFilePath,
|
||||
targetIp: targetIp,
|
||||
command: command,
|
||||
),
|
||||
),
|
||||
await processManager
|
||||
.run(
|
||||
getSshArguments(
|
||||
identityFilePath: identityFilePath,
|
||||
targetIp: targetIp,
|
||||
command: command,
|
||||
),
|
||||
)
|
||||
.timeout(timeoutMs),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:io';
|
||||
import 'dart:io' as io;
|
||||
|
||||
import 'package:file/file.dart';
|
||||
import 'package:file/local.dart';
|
||||
@ -11,6 +11,13 @@ import 'package:process/process.dart';
|
||||
|
||||
import 'operation_result.dart';
|
||||
|
||||
/// Function signature for a [SshKeyManager] provider.
|
||||
typedef SshKeyManagerProvider = SshKeyManager Function({
|
||||
ProcessManager processManager,
|
||||
FileSystem fs,
|
||||
String publicKeyPath,
|
||||
});
|
||||
|
||||
/// A wrapper for managing SSH key generation.
|
||||
///
|
||||
/// Implemented by [SystemSshKeyManager].
|
||||
@ -32,9 +39,23 @@ class SystemSshKeyManager implements SshKeyManager {
|
||||
const SystemSshKeyManager({
|
||||
this.processManager = const LocalProcessManager(),
|
||||
this.fs = const LocalFileSystem(),
|
||||
this.pkeyPubPath,
|
||||
}) : assert(processManager != null),
|
||||
assert(fs != null);
|
||||
|
||||
/// Creates a static provider that returns a SystemSshKeyManager.
|
||||
static SshKeyManager defaultProvider({
|
||||
ProcessManager processManager,
|
||||
FileSystem fs,
|
||||
String publicKeyPath,
|
||||
}) {
|
||||
return SystemSshKeyManager(
|
||||
processManager: processManager ?? const LocalProcessManager(),
|
||||
fs: fs,
|
||||
pkeyPubPath: publicKeyPath,
|
||||
);
|
||||
}
|
||||
|
||||
/// The [ProcessManager] implementation to use when spawning ssh-keygen.
|
||||
final ProcessManager processManager;
|
||||
|
||||
@ -42,6 +63,16 @@ class SystemSshKeyManager implements SshKeyManager {
|
||||
/// file.
|
||||
final FileSystem fs;
|
||||
|
||||
/// The [String] with the path to a public key.
|
||||
final String pkeyPubPath;
|
||||
|
||||
/// Populates [authorizedKeys] file with the public key in [pKeyPub].
|
||||
Future<void> createAuthorizedKeys(File authorizedKeys, File pkeyPub) async {
|
||||
final List<String> pkeyPubParts = pkeyPub.readAsStringSync().split(' ');
|
||||
await authorizedKeys
|
||||
.writeAsString('${pkeyPubParts[0]} ${pkeyPubParts[1]}\n');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<OperationResult> createKeys({
|
||||
String destinationPath = '.ssh',
|
||||
@ -58,9 +89,14 @@ class SystemSshKeyManager implements SshKeyManager {
|
||||
}
|
||||
|
||||
await sshDir.create();
|
||||
if (pkeyPubPath != null) {
|
||||
await createAuthorizedKeys(authorizedKeys, fs.file(pkeyPubPath));
|
||||
return OperationResult.success(info: 'Using previously generated keys.');
|
||||
}
|
||||
|
||||
final File pkey = sshDir.childFile('pkey');
|
||||
final File pkeyPub = sshDir.childFile('pkey.pub');
|
||||
final ProcessResult result = await processManager.run(
|
||||
final io.ProcessResult result = await processManager.run(
|
||||
<String>[
|
||||
'ssh-keygen',
|
||||
'-t', 'ed25519', //
|
||||
@ -72,10 +108,7 @@ class SystemSshKeyManager implements SshKeyManager {
|
||||
if (result.exitCode != 0) {
|
||||
return OperationResult.fromProcessResult(result);
|
||||
}
|
||||
|
||||
final List<String> pkeyPubParts = pkeyPub.readAsStringSync().split(' ');
|
||||
await authorizedKeys
|
||||
.writeAsString('${pkeyPubParts[0]} ${pkeyPubParts[1]}\n');
|
||||
await createAuthorizedKeys(authorizedKeys, pkeyPub);
|
||||
return OperationResult.fromProcessResult(result);
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,9 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:file/file.dart';
|
||||
import 'package:file/memory.dart';
|
||||
import 'package:fuchsia_ctl/fuchsia_ctl.dart';
|
||||
@ -17,20 +20,33 @@ import 'fakes.dart';
|
||||
|
||||
void main() {
|
||||
const String deviceName = 'some-name-to-use';
|
||||
FakeSshKeyManager sshKeyManager;
|
||||
final MemoryFileSystem fs = MemoryFileSystem(style: FileSystemStyle.posix);
|
||||
FakeTar tar;
|
||||
MockProcessManager processManager;
|
||||
SshKeyManagerProvider sshKeyManagerProvider;
|
||||
|
||||
setUp(() {
|
||||
processManager = MockProcessManager();
|
||||
sshKeyManager = const FakeSshKeyManager(true);
|
||||
sshKeyManagerProvider = ({
|
||||
ProcessManager processManager,
|
||||
FileSystem fs,
|
||||
String publicKeyPath,
|
||||
}) {
|
||||
return sshKeyManager;
|
||||
};
|
||||
});
|
||||
|
||||
test('Tar fails', () async {
|
||||
const FakeSshKeyManager sshKeyManager = FakeSshKeyManager(true);
|
||||
final MemoryFileSystem fs = MemoryFileSystem(style: FileSystemStyle.posix);
|
||||
final FakeTar tar = FakeTar(false, fs);
|
||||
final MockProcessManager processManager = MockProcessManager();
|
||||
|
||||
tar = FakeTar(false, fs);
|
||||
when(processManager.start(any)).thenAnswer((_) async {
|
||||
return FakeProcess(0, <String>['Good job'], <String>['']);
|
||||
});
|
||||
|
||||
final ImagePaver paver = ImagePaver(
|
||||
tar: tar,
|
||||
sshKeyManager: sshKeyManager,
|
||||
sshKeyManagerProvider: sshKeyManagerProvider,
|
||||
fs: fs,
|
||||
processManager: processManager,
|
||||
);
|
||||
@ -47,18 +63,15 @@ void main() {
|
||||
});
|
||||
|
||||
test('Ssh fails', () async {
|
||||
const FakeSshKeyManager sshKeyManager = FakeSshKeyManager(false);
|
||||
final MemoryFileSystem fs = MemoryFileSystem(style: FileSystemStyle.posix);
|
||||
final FakeTar tar = FakeTar(true, fs);
|
||||
final MockProcessManager processManager = MockProcessManager();
|
||||
|
||||
sshKeyManager = const FakeSshKeyManager(false);
|
||||
tar = FakeTar(true, fs);
|
||||
when(processManager.start(any)).thenAnswer((_) async {
|
||||
return FakeProcess(0, <String>['Good job'], <String>['']);
|
||||
});
|
||||
|
||||
final ImagePaver paver = ImagePaver(
|
||||
tar: tar,
|
||||
sshKeyManager: sshKeyManager,
|
||||
sshKeyManagerProvider: sshKeyManagerProvider,
|
||||
fs: fs,
|
||||
processManager: processManager,
|
||||
);
|
||||
@ -74,19 +87,40 @@ void main() {
|
||||
expect(result.error, 'ssh failed');
|
||||
});
|
||||
|
||||
test('Happy path', () async {
|
||||
const FakeSshKeyManager sshKeyManager = FakeSshKeyManager(true);
|
||||
final MemoryFileSystem fs = MemoryFileSystem(style: FileSystemStyle.posix);
|
||||
final FakeTar tar = FakeTar(true, fs);
|
||||
final MockProcessManager processManager = MockProcessManager();
|
||||
test('Pave times out', () async {
|
||||
tar = FakeTar(true, fs);
|
||||
|
||||
when(processManager.start(any)).thenAnswer((_) async {
|
||||
await Future<void>.delayed(const Duration(milliseconds: 3));
|
||||
return null;
|
||||
});
|
||||
|
||||
final ImagePaver paver = ImagePaver(
|
||||
tar: tar,
|
||||
sshKeyManagerProvider: sshKeyManagerProvider,
|
||||
fs: fs,
|
||||
processManager: processManager,
|
||||
);
|
||||
|
||||
expect(
|
||||
paver.pave(
|
||||
'generic-x64.tgz',
|
||||
deviceName,
|
||||
verbose: false,
|
||||
timeoutMs: const Duration(milliseconds: 1),
|
||||
),
|
||||
throwsA(const TypeMatcher<TimeoutException>()));
|
||||
});
|
||||
|
||||
test('Happy path', () async {
|
||||
tar = FakeTar(true, fs);
|
||||
when(processManager.start(any)).thenAnswer((_) async {
|
||||
return FakeProcess(0, <String>['Good job'], <String>['']);
|
||||
});
|
||||
|
||||
final ImagePaver paver = ImagePaver(
|
||||
tar: tar,
|
||||
sshKeyManager: sshKeyManager,
|
||||
sshKeyManagerProvider: sshKeyManagerProvider,
|
||||
fs: fs,
|
||||
processManager: processManager,
|
||||
);
|
||||
|
@ -2,6 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io' show ProcessResult;
|
||||
|
||||
import 'package:fuchsia_ctl/fuchsia_ctl.dart';
|
||||
@ -87,6 +88,25 @@ void main() {
|
||||
);
|
||||
expect(args.last, 'ls -al');
|
||||
});
|
||||
|
||||
test('sshCommand times out', () {
|
||||
final MockProcessManager processManager = MockProcessManager();
|
||||
|
||||
when(processManager.run(any)).thenAnswer((_) async {
|
||||
await Future<void>.delayed(const Duration(milliseconds: 3));
|
||||
return ProcessResult(0, 0, 'Good job', '');
|
||||
});
|
||||
|
||||
final SshClient ssh = SshClient(processManager: processManager);
|
||||
expect(
|
||||
ssh.runCommand(
|
||||
targetIp,
|
||||
identityFilePath: identityFilePath,
|
||||
command: const <String>['ls', '-al'],
|
||||
timeoutMs: const Duration(milliseconds: 1),
|
||||
),
|
||||
throwsA(const TypeMatcher<TimeoutException>()));
|
||||
});
|
||||
}
|
||||
|
||||
class MockProcessManager extends Mock implements ProcessManager {}
|
||||
|
62
packages/fuchsia_ctl/test/ssh_key_manager_test.dart
Normal file
62
packages/fuchsia_ctl/test/ssh_key_manager_test.dart
Normal file
@ -0,0 +1,62 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:file/file.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:file/memory.dart';
|
||||
import 'package:fuchsia_ctl/src/ssh_key_manager.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
import 'package:process/process.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
group('SystemSshKeyManager', () {
|
||||
MemoryFileSystem fs;
|
||||
final MockProcessManager processManager = MockProcessManager();
|
||||
|
||||
setUp(() async {
|
||||
fs = MemoryFileSystem();
|
||||
});
|
||||
test('CreateAuthorizedKeys', () async {
|
||||
const SystemSshKeyManager systemSshKeyManager = SystemSshKeyManager();
|
||||
final File authorizedKeys = fs.file('authorized_keys');
|
||||
final File pkeyPub = fs.file('key.pub');
|
||||
pkeyPub.writeAsString('ssh-rsa AAAA== abc@cde.com');
|
||||
systemSshKeyManager.createAuthorizedKeys(authorizedKeys, pkeyPub);
|
||||
final String result = await authorizedKeys.readAsString();
|
||||
expect(result, equals('ssh-rsa AAAA==\n'));
|
||||
});
|
||||
|
||||
test('KeysNotGenerated_PubKeyPassed', () async {
|
||||
final File pkeyPub = fs.file('key.pub');
|
||||
pkeyPub.writeAsString('ssh-rsa AAAA== abc@cde.com');
|
||||
final SystemSshKeyManager systemSshKeyManager = SystemSshKeyManager(
|
||||
processManager: processManager, fs: fs, pkeyPubPath: pkeyPub.path);
|
||||
await systemSshKeyManager.createKeys();
|
||||
final File authorizedKeys = fs.file(path.join('.ssh', 'authorized_keys'));
|
||||
final String result = await authorizedKeys.readAsString();
|
||||
expect(result, equals('ssh-rsa AAAA==\n'));
|
||||
verifyNever(processManager.run(any));
|
||||
});
|
||||
|
||||
test('KeysGenerated', () async {
|
||||
when(processManager.run(any)).thenAnswer((_) async {
|
||||
final File pkeyPub = fs.file(path.join('.ssh', 'pkey.pub'));
|
||||
pkeyPub.writeAsString('ssh-rsa AAAA== abc@cde.com');
|
||||
final File pkey = fs.file(path.join('.ssh', 'pkey'));
|
||||
pkey.create();
|
||||
return ProcessResult(0, 0, 'Good job', '');
|
||||
});
|
||||
final SystemSshKeyManager systemSshKeyManager =
|
||||
SystemSshKeyManager(processManager: processManager, fs: fs);
|
||||
await systemSshKeyManager.createKeys();
|
||||
final File authorizedKeys = fs.file(path.join('.ssh', 'authorized_keys'));
|
||||
expect(await authorizedKeys.exists(), isTrue);
|
||||
final File pkey = fs.file(path.join('.ssh', 'pkey'));
|
||||
expect(await pkey.exists(), isTrue);
|
||||
final File pkeyPub = fs.file(path.join('.ssh', 'pkey.pub'));
|
||||
expect(await pkeyPub.exists(), isTrue);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class MockProcessManager extends Mock implements ProcessManager {}
|
Reference in New Issue
Block a user