247 lines
8.3 KiB
Python
247 lines
8.3 KiB
Python
# -*- 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.api_lib.compute import lister
|
|
from googlecloudsdk.calliope import base
|
|
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 Google Cloud CLI 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 _ContainsPort22(allowed_ports):
|
|
"""Checks if the given list of allowed ports contains port 22.
|
|
|
|
Args:
|
|
allowed_ports:
|
|
|
|
Returns:
|
|
|
|
Raises:
|
|
ValueError:Port value must be of type string.
|
|
"""
|
|
|
|
for port in allowed_ports:
|
|
try:
|
|
if not isinstance(port, str):
|
|
raise ValueError('Port value must be of type string')
|
|
except ValueError as e:
|
|
print(e)
|
|
if port == '22':
|
|
return True
|
|
if '-' in port:
|
|
start = int(port.split('-')[0])
|
|
end = int(port.split('-')[1])
|
|
if start <= 22 <= end:
|
|
return True
|
|
return False
|
|
|
|
|
|
def PopulatePublicKey(
|
|
api_client,
|
|
service_id,
|
|
version_id,
|
|
instance_id,
|
|
public_key,
|
|
oslogin_state_user,
|
|
oslogin_state_enabled
|
|
):
|
|
"""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.
|
|
oslogin_state_user: str, The user to connect as.
|
|
oslogin_state_enabled: bool, Whether OS Login is enabled.
|
|
|
|
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 = oslogin_state_user
|
|
instance_ip_mode_enum = (
|
|
api_client.messages.Network.InstanceIpModeValueValuesEnum)
|
|
host = (
|
|
instance.id if
|
|
version.version.network.instanceIpMode is instance_ip_mode_enum.INTERNAL
|
|
else instance.vmIp)
|
|
remote = ssh.Remote(host=host, user=user)
|
|
if not oslogin_state_enabled:
|
|
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)
|
|
|
|
|
|
def FetchFirewallRules():
|
|
"""Fetches the firewall rules for the current project.
|
|
|
|
Returns:
|
|
A list of firewall rules.
|
|
"""
|
|
holder = compute_base_classes.ComputeApiHolder(base.ReleaseTrack.GA)
|
|
client = holder.client
|
|
# pylint: disable=protected-access
|
|
request_data = lister._Frontend(
|
|
None,
|
|
None,
|
|
lister.GlobalScope([
|
|
holder.resources.Parse(
|
|
properties.VALUES.core.project.GetOrFail(),
|
|
collection='compute.projects',
|
|
)
|
|
]),
|
|
)
|
|
list_implementation = lister.GlobalLister(
|
|
client, client.apitools_client.firewalls
|
|
)
|
|
result = lister.Invoke(request_data, list_implementation)
|
|
return result
|
|
|
|
|
|
def FilterFirewallRules(firewall_rules):
|
|
"""Filters firewall rules that allow ingress to port 22."""
|
|
filtered_firewall_rules = []
|
|
for firewall_rule in firewall_rules:
|
|
if firewall_rule.get('direction') == 'INGRESS':
|
|
allowed_dict = firewall_rule.get('allowed')
|
|
if not allowed_dict:
|
|
continue
|
|
allowed_ports = allowed_dict[0].get('ports')
|
|
if not allowed_ports:
|
|
continue
|
|
if _ContainsPort22(allowed_ports):
|
|
filtered_firewall_rules.append(firewall_rule)
|
|
return filtered_firewall_rules
|