| # Copyright 2020 The Fuchsia Authors. All rights reserved. | |
| # Use of this source code is governed by a BSD-style license that can be | |
| # found in the LICENSE file. | |
| import attr | |
| from recipe_engine import recipe_api | |
| from recipe_engine.config_types import Path | |
| _SSH_CONFIG_TEMPLATE = """ | |
| Host * | |
| CheckHostIP no | |
| StrictHostKeyChecking no | |
| ForwardAgent no | |
| ForwardX11 no | |
| UserKnownHostsFile /dev/null | |
| User fuchsia | |
| IdentitiesOnly yes | |
| IdentityFile {identity} | |
| ServerAliveInterval 2 | |
| ServerAliveCountMax 5 | |
| ControlMaster auto | |
| ControlPersist 1m | |
| ControlPath /tmp/ssh-%r@%h:%p | |
| ConnectTimeout 5 | |
| """ | |
| @attr.s | |
| class SSHFilePaths(object): | |
| """Required files to setup SSH on FEMU.""" | |
| # Recipe API, required | |
| _api = attr.ib(type=recipe_api.RecipeApi) | |
| # Files for SSH | |
| host_private = attr.ib(type=Path, default=None) | |
| host_public = attr.ib(type=Path, default=None) | |
| id_private = attr.ib(type=Path, default=None) | |
| id_public = attr.ib(type=Path, default=None) | |
| def _exists(self, p): | |
| return p and self._api.path.exists(p) | |
| def _exist(self): | |
| return all([ | |
| self._exists(self.host_private), | |
| self._exists(self.host_public), | |
| self._exists(self.id_private), | |
| self._exists(self.id_public), | |
| ]) | |
| def _report_missing(self): | |
| result = [] | |
| if not self._exists(self.host_private): | |
| result.append(self.host_private) | |
| if not self._exists(self.host_public): | |
| result.append(self.host_public) | |
| if not self._exists(self.id_private): | |
| result.append(self.id_private) | |
| if not self._exists(self.id_public): | |
| result.append(self.id_public) | |
| return result | |
| class SSHApi(recipe_api.RecipeApi): | |
| def __init__(self, *args, **kwargs): | |
| super(SSHApi, self).__init__(*args, **kwargs) | |
| self._ssh_paths = None | |
| def _create_ssh_keys(self, timeout_secs=10 * 60): | |
| """Generate private, public key-pairs for Host side and Device side ssh keys.""" | |
| self.m.file.ensure_directory('init ssh cache', self.ssh_cache_root) | |
| if not self._ssh_paths: | |
| self._ssh_paths = SSHFilePaths( | |
| api=self.m, | |
| host_private=self.ssh_cache_root.join('ssh_host_key'), | |
| host_public=self.ssh_cache_root.join('ssh_host_key.pub'), | |
| id_private=self.ssh_cache_root.join('id_ed25519'), | |
| id_public=self.ssh_cache_root.join('id_ed25519.pub'), | |
| ) | |
| if not self.m.file.listdir(name='check ssh cache content', | |
| source=self.ssh_cache_root, test_data=()): | |
| self.m.step( | |
| 'ssh-keygen host', | |
| [ | |
| 'ssh-keygen', | |
| '-t', | |
| 'ed25519', | |
| '-h', | |
| '-f', | |
| self._ssh_paths.host_private, | |
| '-P', | |
| '', | |
| '-N', | |
| '', | |
| ], | |
| infra_step=True, | |
| timeout=timeout_secs, | |
| ) | |
| self.m.step( | |
| 'ssh-keygen device', | |
| [ | |
| 'ssh-keygen', | |
| '-t', | |
| 'ed25519', | |
| '-f', | |
| self._ssh_paths.id_private, | |
| '-P', | |
| '', | |
| '-N', | |
| '', | |
| ], | |
| infra_step=True, | |
| timeout=timeout_secs, | |
| ) | |
| return self._ssh_paths | |
| def generate_ssh_config(self, private_key_path, dest): | |
| """Generates and sets the private_key_path in ssh_config file.""" | |
| self.m.file.write_text( | |
| name='generate ssh_config at %s' % dest, | |
| dest=dest, | |
| text_data=_SSH_CONFIG_TEMPLATE.format(identity=private_key_path), | |
| ) | |
| @property | |
| def ssh_paths(self): | |
| """Generate SSH keys. | |
| Raises: | |
| StepFailure: When ssh key file paths do not exist. | |
| """ | |
| self._create_ssh_keys() | |
| if not self._ssh_paths._exist(): | |
| missing = self._ssh_paths._report_missing() | |
| ex = self.m.step.StepFailure( | |
| 'SSH paths do not exist. {missing_paths}'.format( | |
| missing_paths=missing | |
| ) | |
| ) | |
| ex.missing_paths = missing | |
| raise ex | |
| return self._ssh_paths | |
| @property | |
| def ssh_cache_root(self): | |
| return self.m.buildbucket.builder_cache_path.join('ssh') |