Skip to content

Commit daebadc

Browse files
authored
Merge branch 'master' into patch-7
2 parents ffcb930 + 424a14c commit daebadc

30 files changed

+1258
-29
lines changed

compute/oslogin/requirements.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
google-api-python-client==1.7.4
2+
google-auth==1.6.1
3+
google-auth-httplib2==0.0.3
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
#!/usr/bin/env python
2+
3+
# Copyright 2018 Google Inc. All Rights Reserved.
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
"""Example of using the OS Login API to apply public SSH keys for a service
18+
account, and use that service account to execute commands on a remote
19+
instance over SSH. This example uses zonal DNS names to address instances
20+
on the same internal VPC network.
21+
"""
22+
23+
# [START imports_and_variables]
24+
import time
25+
import subprocess
26+
import uuid
27+
import logging
28+
import requests
29+
import argparse
30+
import googleapiclient.discovery
31+
32+
# Global variables
33+
SERVICE_ACCOUNT_METADATA_URL = (
34+
'http://metadata.google.internal/computeMetadata/v1/instance/'
35+
'service-accounts/default/email')
36+
HEADERS = {'Metadata-Flavor': 'Google'}
37+
38+
# [END imports_and_variables]
39+
40+
41+
# [START run_command_local]
42+
def execute(cmd, cwd=None, capture_output=False, env=None, raise_errors=True):
43+
"""Execute an external command (wrapper for Python subprocess)."""
44+
logging.info('Executing command: {cmd}'.format(cmd=str(cmd)))
45+
stdout = subprocess.PIPE if capture_output else None
46+
process = subprocess.Popen(cmd, cwd=cwd, env=env, stdout=stdout)
47+
output = process.communicate()[0]
48+
returncode = process.returncode
49+
if returncode:
50+
# Error
51+
if raise_errors:
52+
raise subprocess.CalledProcessError(returncode, cmd)
53+
else:
54+
logging.info('Command returned error status %s', returncode)
55+
if output:
56+
logging.info(output)
57+
return returncode, output
58+
# [END run_command_local]
59+
60+
61+
# [START create_key]
62+
def create_ssh_key(oslogin, account, private_key_file=None, expire_time=300):
63+
"""Generate an SSH key pair and apply it to the specified account."""
64+
private_key_file = private_key_file or '/tmp/key-' + str(uuid.uuid4())
65+
execute(['ssh-keygen', '-t', 'rsa', '-N', '', '-f', private_key_file])
66+
67+
with open(private_key_file + '.pub', 'r') as original:
68+
public_key = original.read().strip()
69+
70+
# Expiration time is in microseconds.
71+
expiration = int((time.time() + expire_time) * 1000000)
72+
73+
body = {
74+
'key': public_key,
75+
'expirationTimeUsec': expiration,
76+
}
77+
oslogin.users().importSshPublicKey(parent=account, body=body).execute()
78+
return private_key_file
79+
# [END create_key]
80+
81+
82+
# [START run_command_remote]
83+
def run_ssh(cmd, private_key_file, username, hostname):
84+
"""Run a command on a remote system."""
85+
ssh_command = [
86+
'ssh', '-i', private_key_file, '-o', 'StrictHostKeyChecking=no',
87+
'{username}@{hostname}'.format(username=username, hostname=hostname),
88+
cmd,
89+
]
90+
ssh = subprocess.Popen(
91+
ssh_command, shell=False, stdout=subprocess.PIPE,
92+
stderr=subprocess.PIPE)
93+
result = ssh.stdout.readlines()
94+
return result if result else ssh.stderr.readlines()
95+
96+
# [END run_command_remote]
97+
98+
99+
# [START main]
100+
def main(cmd, project, instance=None, zone=None,
101+
oslogin=None, account=None, hostname=None):
102+
"""Run a command on a remote system."""
103+
104+
# Create the OS Login API object.
105+
oslogin = oslogin or googleapiclient.discovery.build('oslogin', 'v1')
106+
107+
# Identify the service account ID if it is not already provided.
108+
account = account or requests.get(
109+
SERVICE_ACCOUNT_METADATA_URL, headers=HEADERS).text
110+
account = 'users/' + account
111+
112+
# Create a new SSH key pair and associate it with the service account.
113+
private_key_file = create_ssh_key(oslogin, account)
114+
115+
# Using the OS Login API, get the POSIX user name from the login profile
116+
# for the service account.
117+
profile = oslogin.users().getLoginProfile(name=account).execute()
118+
username = profile.get('posixAccounts')[0].get('username')
119+
120+
# Create the hostname of the target instance using the instance name,
121+
# the zone where the instance is located, and the project that owns the
122+
# instance.
123+
hostname = hostname or '{instance}.{zone}.c.{project}.internal'.format(
124+
instance=instance, zone=zone, project=project)
125+
126+
# Run a command on the remote instance over SSH.
127+
result = run_ssh(cmd, private_key_file, username, hostname)
128+
129+
# Print the command line output from the remote instance.
130+
# Use .rstrip() rather than end='' for Python 2 compatability.
131+
for line in result:
132+
print(line.decode('utf-8').rstrip('\n\r'))
133+
134+
# Shred the private key and delete the pair.
135+
execute(['shred', private_key_file])
136+
execute(['rm', private_key_file])
137+
execute(['rm', private_key_file + '.pub'])
138+
139+
140+
if __name__ == '__main__':
141+
142+
parser = argparse.ArgumentParser(
143+
description=__doc__,
144+
formatter_class=argparse.RawDescriptionHelpFormatter)
145+
parser.add_argument(
146+
'--cmd', default='uname -a',
147+
help='The command to run on the remote instance.')
148+
parser.add_argument(
149+
'--project',
150+
help='Your Google Cloud project ID.')
151+
parser.add_argument(
152+
'--zone',
153+
help='The zone where the target instance is locted.')
154+
parser.add_argument(
155+
'--instance',
156+
help='The target instance for the ssh command.')
157+
parser.add_argument(
158+
'--account',
159+
help='The service account email.')
160+
parser.add_argument(
161+
'--hostname',
162+
help='The external IP address or hostname for the target instance.')
163+
args = parser.parse_args()
164+
165+
main(args.cmd, args.project, instance=args.instance, zone=args.zone,
166+
account=args.account, hostname=args.hostname)
167+
168+
# [END main]

0 commit comments

Comments
 (0)