File: //snap/google-cloud-cli/394/lib/surface/app/instances/ssh.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.
"""The `app instances ssh` command."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import argparse
import textwrap
from apitools.base.py import exceptions as apitools_exceptions
from googlecloudsdk.api_lib.app import appengine_api_client
from googlecloudsdk.api_lib.compute import base_classes as compute_base_classes
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.app import exceptions as command_exceptions
from googlecloudsdk.command_lib.app import flags
from googlecloudsdk.command_lib.app import iap_tunnel
from googlecloudsdk.command_lib.app import ssh_common
from googlecloudsdk.command_lib.util.ssh import containers
from googlecloudsdk.command_lib.util.ssh import ssh
from googlecloudsdk.core import log
from googlecloudsdk.core import properties
from googlecloudsdk.core import resources
def _ArgsCommon(parser):
parser.add_argument(
'instance',
help='The instance ID.')
parser.add_argument(
'--container',
help='Name of the container within the VM to connect to.')
parser.add_argument(
'command',
nargs=argparse.REMAINDER,
help='Remote command to execute on the VM.')
@base.ReleaseTracks(base.ReleaseTrack.GA)
@base.DefaultUniverseOnly
class SshGa(base.Command):
"""SSH into the VM of an App Engine Flexible instance."""
detailed_help = {
'DESCRIPTION':
textwrap.dedent("""\
*{command}* lets you remotely log in to your running App Engine Flexible
instances under two conditions:
* The instance is running.
* The instance has an external IP address. To check from the Cloud
Console, go to the Instances page and confirm that there is an IP
address listed in the VM IP column. To check from your app.yaml, open
your app.yaml and look at the network settings. The *instance_ip_mode*
field must be either not listed or set to ``external''.""") +
ssh_common.DETAILED_HELP,
'EXAMPLES':
"""\
To SSH into an App Engine Flexible instance, run:
$ {command} --service=s1 --version=v1 i1
To SSH into the app container within an instance, run:
$ {command} --service=s1 --version=v1 i1 --container=gaeapp
To SSH into the app container and run a remote command, run:
$ {command} --service=s1 --version=v1 i1 --container=gaeapp -- echo hello
""",
}
@staticmethod
def Args(parser):
flags.AddServiceVersionSelectArgs(parser)
_ArgsCommon(parser)
iap_tunnel.AddSshTunnelArgs(parser)
def Run(self, args):
"""Connect to a running App Engine flexible instance.
Args:
args: argparse.Namespace, the args the command was invoked with.
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.
ssh.CommandError: The SSH command exited with SSH exit code, which
usually implies that a connection problem occurred.
Returns:
int, The exit code of the SSH command.
"""
api_client = appengine_api_client.GetApiClientForTrack(self.ReleaseTrack())
try:
res = resources.REGISTRY.Parse(
args.instance,
collection='appengine.apps.services.versions.instances')
service = res.servicesId
version = res.versionsId
instance = res.instancesId
except resources.RequiredFieldOmittedException:
service = args.service
version = args.version
instance = args.instance
env = ssh.Environment.Current()
env.RequireSSH()
keys = ssh.Keys.FromFilename()
keys.EnsureKeysExist(overwrite=False)
try:
version_resource = api_client.GetVersionResource(service, version)
except apitools_exceptions.HttpNotFoundError:
raise command_exceptions.MissingVersionError('{}/{}'.format(
service, version))
project = properties.VALUES.core.project.GetOrFail()
res = resources.REGISTRY.Parse(
instance,
params={
'appsId': project,
'versionsId': version,
'instancesId': instance,
'servicesId': service,
},
collection='appengine.apps.services.versions.instances',
)
try:
instance_resource = api_client.GetInstanceResource(res)
except apitools_exceptions.HttpNotFoundError:
raise command_exceptions.MissingInstanceError(res.RelativeName())
iap_tunnel_args = iap_tunnel.CreateSshTunnelArgs(
args,
api_client,
self.ReleaseTrack(),
project,
version_resource,
instance_resource,
)
region = '-'.join(instance_resource.vmZoneName.split('-')[:-1])
user = ssh.GetDefaultSshUsername()
project = ssh_common.GetComputeProject(self.ReleaseTrack())
oslogin_state = ssh.GetOsloginState(
None,
project,
user,
keys.GetPublicKey().ToEntry(),
None,
self.ReleaseTrack(),
app_engine_params={
'appsId': project.name,
'servicesId': service,
'versionsId': version,
'instancesId': instance,
'serviceAccount': (
version_resource.serviceAccount
if version_resource.serviceAccount
else ''
),
'region': region
},
messages=compute_base_classes.ComputeApiHolder(
self.ReleaseTrack()
).client.messages,
)
cert_file = None
if oslogin_state.third_party_user or oslogin_state.require_certificates:
cert_file = ssh.CertFileFromAppEngineInstance(
project.name, service, version, instance
)
connection_details = ssh_common.PopulatePublicKey(
api_client,
service,
version,
instance,
keys.GetPublicKey(),
oslogin_state.user,
oslogin_state.oslogin_enabled,
)
remote_command = containers.GetRemoteCommand(args.container, args.command)
tty = containers.GetTty(args.container, args.command)
try:
filtered_firewall_rules = ssh_common.FilterFirewallRules(
ssh_common.FetchFirewallRules()
)
if not filtered_firewall_rules:
log.warning(
'No ingress firewall rule that allows ingress to port 22. '
'User should add a firewall rule that allows ingress to port 22.'
)
except apitools_exceptions.NotFoundError:
raise log.exception('Unable to fetch firewall rules')
return ssh.SSHCommand(
connection_details.remote,
identity_file=keys.key_file,
cert_file=cert_file,
tty=tty,
remote_command=remote_command,
options=connection_details.options,
iap_tunnel_args=iap_tunnel_args).Run(env)
@base.ReleaseTracks(base.ReleaseTrack.BETA)
@base.DefaultUniverseOnly
class SshBeta(SshGa):
"""SSH into the VM of an App Engine Flexible instance."""
@staticmethod
def Args(parser):
flags.AddServiceVersionSelectArgs(parser, short_flags=True)
_ArgsCommon(parser)
iap_tunnel.AddSshTunnelArgs(parser)
def Run(self, args):
"""Connect to a running App Engine flexible instance.
Args:
args: argparse.Namespace, the args the command was invoked with.
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.
ssh.CommandError: The SSH command exited with SSH exit code, which
usually implies that a connection problem occurred.
Returns:
int, The exit code of the SSH command.
"""
log.warning(
'For `gcloud beta app instances ssh`, the short flags `-s` and `-v` '
'are deprecated and will be removed 2017-09-27. For the GA command, '
'they are not available. Please use `--service` and `--version` '
'instead.')
api_client = appengine_api_client.GetApiClientForTrack(self.ReleaseTrack())
try:
res = resources.REGISTRY.Parse(
args.instance,
collection='appengine.apps.services.versions.instances')
service = res.servicesId
version = res.versionsId
instance = res.instancesId
except resources.RequiredFieldOmittedException:
service = args.service
version = args.version
instance = args.instance
env = ssh.Environment.Current()
env.RequireSSH()
keys = ssh.Keys.FromFilename()
keys.EnsureKeysExist(overwrite=False)
try:
version_resource = api_client.GetVersionResource(service, version)
except apitools_exceptions.HttpNotFoundError:
raise command_exceptions.MissingVersionError('{}/{}'.format(
service, version))
project = properties.VALUES.core.project.GetOrFail()
res = resources.REGISTRY.Parse(
instance,
params={
'appsId': project,
'versionsId': version,
'instancesId': instance,
'servicesId': service,
},
collection='appengine.apps.services.versions.instances')
try:
instance_resource = api_client.GetInstanceResource(res)
except apitools_exceptions.HttpNotFoundError:
raise command_exceptions.MissingInstanceError(res.RelativeName())
iap_tunnel_args = iap_tunnel.CreateSshTunnelArgs(args, api_client,
self.ReleaseTrack(),
project, version_resource,
instance_resource)
region = '-'.join(instance_resource.vmZoneName.split('-')[:-1])
user = ssh.GetDefaultSshUsername()
project = ssh_common.GetComputeProject(self.ReleaseTrack())
oslogin_state = ssh.GetOsloginState(
None,
project,
user,
keys.GetPublicKey().ToEntry(),
None,
self.ReleaseTrack(),
app_engine_params={
'appsId': project.name,
'servicesId': service,
'versionsId': version,
'instancesId': instance,
'serviceAccount': (
version_resource.serviceAccount
if version_resource.serviceAccount
else ''
),
'region': region
},
messages=compute_base_classes.ComputeApiHolder(
self.ReleaseTrack()
).client.messages,
)
cert_file = None
if oslogin_state.third_party_user or oslogin_state.require_certificates:
cert_file = ssh.CertFileFromAppEngineInstance(
project.name, service, version, instance
)
connection_details = ssh_common.PopulatePublicKey(
api_client,
service,
version,
instance,
keys.GetPublicKey(),
oslogin_state.user,
oslogin_state.oslogin_enabled,
)
remote_command = containers.GetRemoteCommand(args.container, args.command)
tty = containers.GetTty(args.container, args.command)
return ssh.SSHCommand(
connection_details.remote,
identity_file=keys.key_file,
cert_file=cert_file,
tty=tty,
remote_command=remote_command,
options=connection_details.options,
iap_tunnel_args=iap_tunnel_args).Run(env)