File: //lib/google-cloud-sdk/lib/googlecloudsdk/command_lib/app/ssh_common.py
# -*- coding: utf-8 -*- #
# Copyright 2017 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Utilities for `app instances *` commands using SSH."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from apitools.base.py import exceptions as apitools_exceptions
from googlecloudsdk.api_lib.app import env
from googlecloudsdk.api_lib.app import version_util
from googlecloudsdk.api_lib.compute import base_classes as compute_base_classes
from googlecloudsdk.command_lib.app import exceptions as command_exceptions
from googlecloudsdk.command_lib.projects import util as projects_util
from googlecloudsdk.command_lib.util.ssh import ssh
from googlecloudsdk.core import log
from googlecloudsdk.core import properties
from googlecloudsdk.core import resources
from googlecloudsdk.core.console import console_io
_ENABLE_DEBUG_WARNING = """\
This instance is serving live application traffic. Any changes made could
result in downtime or unintended consequences."""
# Used by OpenSSH for naming a logical host in the known_hosts file, rather than
# relying on IP or DNS. Flexible instance IDs are unique per project.
_HOST_KEY_ALIAS = 'gae.{project}.{instance_id}'
DETAILED_HELP = """
*{command}* resolves the instance's IP address and pre-populates the
VM with a public key managed by gcloud. If the gcloud managed key pair
does not exist, it is generated the first time an SSH command is run,
which may prompt you for a passphrase for the private key encryption.
All SSH commands require the OpenSSH client suite to be installed on
Linux and Mac OS X. On Windows, the Cloud SDK comes with a bundled
PuTTY suite instead, so it has no external dependencies."""
class ConnectionDetails(object):
"""Details about an SSH connection, for assembling an SSH command."""
def __init__(self, remote, options):
self.remote = remote
self.options = options
def __eq__(self, other):
if isinstance(other, self.__class__):
return self.__dict__ == other.__dict__
return False
def __ne__(self, other):
return not self.__eq__(other)
def __repr__(self):
return 'ConnectionDetails(**{})'.format(self.__dict__)
def _GetComputeProject(release_track):
holder = compute_base_classes.ComputeApiHolder(release_track)
client = holder.client
project_ref = projects_util.ParseProject(
properties.VALUES.core.project.GetOrFail())
return client.MakeRequests([(client.apitools_client.projects, 'Get',
client.messages.ComputeProjectsGetRequest(
project=project_ref.projectId))])[0]
def PopulatePublicKey(api_client, service_id, version_id, instance_id,
public_key, release_track):
"""Enable debug mode on and send SSH keys to a flex instance.
Common method for SSH-like commands, does the following:
- Makes sure that the service/version/instance specified exists and is of the
right type (Flexible).
- If not already done, prompts and enables debug on the instance.
- Populates the public key onto the instance.
Args:
api_client: An appengine_api_client.AppEngineApiClient.
service_id: str, The service ID.
version_id: str, The version ID.
instance_id: str, The instance ID.
public_key: ssh.Keys.PublicKey, Public key to send.
release_track: calliope.base.ReleaseTrack, The current release track.
Raises:
InvalidInstanceTypeError: The instance is not supported for SSH.
MissingVersionError: The version specified does not exist.
MissingInstanceError: The instance specified does not exist.
UnattendedPromptError: Not running in a tty.
OperationCancelledError: User cancelled the operation.
Returns:
ConnectionDetails, the details to use for SSH/SCP for the SSH
connection.
"""
try:
version = api_client.GetVersionResource(
service=service_id, version=version_id)
except apitools_exceptions.HttpNotFoundError:
raise command_exceptions.MissingVersionError(
'{}/{}'.format(service_id, version_id))
version = version_util.Version.FromVersionResource(version, None)
if version.environment is not env.FLEX:
if version.environment is env.MANAGED_VMS:
environment = 'Managed VMs'
msg = 'Use `gcloud compute ssh` for Managed VMs instances.'
else:
environment = 'Standard'
msg = None
raise command_exceptions.InvalidInstanceTypeError(environment, msg)
res = resources.REGISTRY.Parse(
instance_id,
params={
'appsId': properties.VALUES.core.project.GetOrFail,
'versionsId': version_id,
'instancesId': instance_id,
'servicesId': service_id,
},
collection='appengine.apps.services.versions.instances')
rel_name = res.RelativeName()
try:
instance = api_client.GetInstanceResource(res)
except apitools_exceptions.HttpNotFoundError:
raise command_exceptions.MissingInstanceError(rel_name)
if not instance.vmDebugEnabled:
log.warning(_ENABLE_DEBUG_WARNING)
console_io.PromptContinue(cancel_on_no=True, throw_if_unattended=True)
user = ssh.GetDefaultSshUsername()
project = _GetComputeProject(release_track)
user, use_oslogin = ssh.CheckForOsloginAndGetUser(
None, project, user, public_key.ToEntry(), None, release_track)
remote = ssh.Remote(instance.vmIp, user=user)
if not use_oslogin:
ssh_key = '{user}:{key} {user}'.format(user=user, key=public_key.ToEntry())
log.status.Print('Sending public key to instance [{}].'.format(rel_name))
api_client.DebugInstance(res, ssh_key)
options = {
'IdentitiesOnly': 'yes', # No ssh-agent as of yet
'UserKnownHostsFile': ssh.KnownHosts.DEFAULT_PATH,
'CheckHostIP': 'no',
'HostKeyAlias': _HOST_KEY_ALIAS.format(project=api_client.project,
instance_id=instance_id)}
return ConnectionDetails(remote, options)