271 lines
9.7 KiB
Python
271 lines
9.7 KiB
Python
# -*- coding: utf-8 -*- #
|
|
# Copyright 2025 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 `gcloud meta daemon` command."""
|
|
|
|
from __future__ import absolute_import
|
|
from __future__ import division
|
|
from __future__ import unicode_literals
|
|
|
|
import http.server
|
|
import json
|
|
import logging
|
|
import os
|
|
import socketserver
|
|
import sys
|
|
import time
|
|
|
|
from googlecloudsdk import gcloud_main
|
|
from googlecloudsdk.calliope import base
|
|
from googlecloudsdk.core.util import files
|
|
|
|
# --- Configuration ---
|
|
HOST = '0.0.0.0'
|
|
PORT = 8080
|
|
PRECOMPUTE_DATA = os.environ.get(
|
|
'GCLOUD_DAEMON_PRECOMPUTE_DATA', 'CLI'
|
|
) # or SURFACES or COMMANDS
|
|
|
|
|
|
def perform_gcloud_execution(cli_obj, command_list):
|
|
"""Executes the gcloud command using the precomputed CLI object."""
|
|
start_time = time.time()
|
|
pid = os.getpid() # Current process PID
|
|
logging.info(
|
|
"PID: %s - Starting perform_gcloud_execution with input: '%s'",
|
|
pid,
|
|
command_list,
|
|
)
|
|
|
|
if cli_obj is None:
|
|
logging.error('PID: %s - Precomputed CLI object is None!', pid)
|
|
return {'error': 'Internal Server Error: CLI object not available'}
|
|
|
|
try:
|
|
request = cli_obj.Execute(command_list)
|
|
response_data = {
|
|
'uri': request.uri,
|
|
'method': request.method,
|
|
'headers': {
|
|
k.decode('utf-8'): v.decode('utf-8')
|
|
for k, v in request.headers.items()
|
|
},
|
|
'body': (
|
|
request.body.decode('utf-8')
|
|
if isinstance(request.body, bytes)
|
|
else request.body
|
|
),
|
|
}
|
|
logging.info('PID: %s - Execution successful.', pid)
|
|
return response_data
|
|
except SystemExit as exc:
|
|
logging.exception('PID: %s - SystemExit processing request: %s', pid, exc)
|
|
exit_code = exc.code if isinstance(exc.code, int) else 1
|
|
return {
|
|
'error': 'Command failed with SystemExit: %s' % exc,
|
|
'exit_code': exit_code,
|
|
}
|
|
except Exception as e: # pylint: disable=broad-exception-caught
|
|
logging.error('PID: %s - Exception during command execution:', pid)
|
|
return {'error': 'Exception during command execution: %s' % str(e)}
|
|
finally:
|
|
end_time = time.time()
|
|
computation_time = end_time - start_time
|
|
logging.info(
|
|
'PID: %s - Completed perform_gcloud_execution in %.4f seconds',
|
|
pid,
|
|
computation_time,
|
|
)
|
|
|
|
|
|
@base.Hidden
|
|
@base.DefaultUniverseOnly
|
|
class Daemon(base.Command):
|
|
"""(DEBUG MODE) Precomputes gcloud CLI and runs a single-request server."""
|
|
|
|
detailed_help = {
|
|
'DESCRIPTION': """
|
|
(DEBUG MODE) Initializes the gcloud CLI environment based on PRECOMPUTE_DATA,
|
|
starts an HTTP server in the FOREGROUND, serves exactly one POST
|
|
request to the root path ('/'), executes the requested gcloud command
|
|
using the precomputed environment, returns the result, and then exits.
|
|
|
|
The command will BLOCK until the single request is received and processed.
|
|
""",
|
|
'EXAMPLES': """
|
|
To run the foreground daemon precomputing the basic CLI:
|
|
|
|
$ gcloud meta daemon
|
|
|
|
(The command will now wait here)
|
|
|
|
Open another terminal and send a request (e.g., using curl):
|
|
|
|
$ curl -X POST -H "Content-Type: application/json" -d '{"command_list": ["projects", "list", "--limit=1", "--format=json"]}' http://localhost:8080/
|
|
|
|
(The original 'gcloud meta daemon' command will process this, print logs, and then exit)
|
|
""",
|
|
}
|
|
|
|
def Run(self, args):
|
|
# Configure logging for the main foreground process
|
|
# pid = os.getpid()
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format='%(asctime)s - PID: %(process)d - %(levelname)s - %(message)s',
|
|
)
|
|
|
|
# --- Precompute CLI in this main process ---
|
|
logging.info('Starting precomputation...')
|
|
precomputation_start_time = time.time()
|
|
try:
|
|
# Set SDK properties needed for precomputation/execution
|
|
os.environ['CLOUDSDK_CORE_DRY_RUN'] = '1'
|
|
os.environ['CLOUDSDK_AUTH_DISABLE_CREDENTIALS'] = '1'
|
|
os.environ['CLOUDSDK_CORE_DISABLE_PROMPTS'] = '1'
|
|
os.environ['CLOUDSDK_CORE_DISABLE_USAGE_REPORTING'] = '1'
|
|
os.environ['CLOUDSDK_COMPONENT_MANAGER_DISABLE_UPDATE_CHECK'] = '1'
|
|
os.environ['CLOUDSDK_CORE_DISABLE_FILE_LOGGING'] = '1'
|
|
os.environ['CLOUDSDK_CORE_SHOW_STRUCTURED_LOGS'] = 'always'
|
|
os.environ['CLOUDSDK_CORE_VERBOSITY'] = 'error'
|
|
os.environ['CLOUDSDK_METRICS_ENVIRONMENT'] = 'gaas'
|
|
os.environ['CLOUDSDK_CORE_USER_OUTPUT_ENABLED'] = '0'
|
|
|
|
precomputed_cli = gcloud_main.CreateCLI([])
|
|
|
|
logging.info('Precomputing basic CLI...')
|
|
logging.info('Loading frequent commands')
|
|
try:
|
|
compute_instances_list_command = [
|
|
'compute',
|
|
'instances',
|
|
'list',
|
|
'--project=fake-project',
|
|
]
|
|
precomputed_cli.Execute(compute_instances_list_command)
|
|
except Exception: # pylint: disable=broad-exception-caught
|
|
pass
|
|
|
|
precomputation_end_time = time.time()
|
|
logging.info(
|
|
'Precomputation complete in %.4f seconds.',
|
|
precomputation_end_time - precomputation_start_time,
|
|
)
|
|
except Exception as e: # pylint: disable=broad-exception-caught
|
|
logging.exception('Failed during CLI precomputation:')
|
|
sys.exit('Error during CLI precomputation: %s' % e)
|
|
|
|
# --- Define Handler and Start Server ---
|
|
class SingleRequestHandler(http.server.BaseHTTPRequestHandler):
|
|
"""Handles ONE incoming POST request and then signals shutdown."""
|
|
|
|
# pylint: disable=invalid-name
|
|
def do_POST(self):
|
|
"""Handles the single POST request."""
|
|
pid = os.getpid() # Log current process PID
|
|
logging.info('PID: %s - Request received at path: %s', pid, self.path)
|
|
|
|
if self.path == '/':
|
|
content_length = int(self.headers['Content-Length'])
|
|
post_data = self.rfile.read(content_length)
|
|
try:
|
|
request_data = json.loads(post_data.decode('utf-8'))
|
|
command_list = request_data.get('command_list')
|
|
if not command_list or not isinstance(command_list, list):
|
|
logging.error(
|
|
"PID: %s - Missing or invalid 'command_list' (must be a list)"
|
|
' in request',
|
|
pid,
|
|
)
|
|
self.send_error(
|
|
400,
|
|
"Missing or invalid 'command_list' (must be a list) in"
|
|
' request',
|
|
)
|
|
return
|
|
except json.JSONDecodeError:
|
|
logging.error('PID: %s - Invalid JSON request', pid, exc_info=True)
|
|
self.send_error(400, 'Invalid JSON request')
|
|
return
|
|
|
|
logging.info(
|
|
'PID: %s - Received compute request with command: %s',
|
|
pid,
|
|
command_list,
|
|
)
|
|
|
|
# Execute the command using the precomputed CLI
|
|
response_data = perform_gcloud_execution(
|
|
precomputed_cli, command_list
|
|
)
|
|
|
|
# Send response
|
|
status_code = 200 # sandbox always returns 200 for IPC
|
|
self.send_response(status_code)
|
|
self.send_header('Content-type', 'application/json')
|
|
self.end_headers()
|
|
self.wfile.write(json.dumps(response_data).encode('utf-8'))
|
|
logging.info('PID: %s - Response sent to client.', pid)
|
|
|
|
else:
|
|
logging.warning(
|
|
'PID: %s - Received request for unknown path: %s', pid, self.path
|
|
)
|
|
self.send_error(404) # Not Found
|
|
|
|
# Signal the server's main thread to shut down
|
|
logging.info(
|
|
'PID: %s - Request handled. Signaling server shutdown.', pid
|
|
)
|
|
|
|
# pylint: disable=redefined-builtin
|
|
def log_message(self, format, *args):
|
|
"""Log messages using the standard logger."""
|
|
# Adjust message slightly for foreground clarity
|
|
logging.info('PID: %s - HTTP Server: %s', os.getpid(), format % args)
|
|
|
|
# --- Start HTTP server directly in this process ---
|
|
logging.info('Starting foreground HTTP server...')
|
|
try:
|
|
socketserver.TCPServer.allow_reuse_address = True
|
|
with socketserver.TCPServer((HOST, PORT), SingleRequestHandler) as httpd:
|
|
# Touch a file to signal readiness
|
|
gcloud_daemon_ready_file = '/tmp/gcloud_daemon.ready'
|
|
with files.FileWriter(gcloud_daemon_ready_file) as f:
|
|
f.write('ready')
|
|
logging.info('Created %s', gcloud_daemon_ready_file)
|
|
logging.info(
|
|
'Server started on http://%s:%s (PRECOMPUTE_DATA=%s)',
|
|
HOST,
|
|
PORT,
|
|
PRECOMPUTE_DATA,
|
|
)
|
|
# Process one request (blocks until one arrives) and then exits the
|
|
# 'with' block, which handles shutdown.
|
|
httpd.handle_request()
|
|
|
|
logging.info('Request handled. Server shutting down.')
|
|
httpd.server_close()
|
|
|
|
except Exception as e: # pylint: disable=broad-exception-caught
|
|
# Log any exception during server setup or the loop itself
|
|
logging.exception('Unhandled exception occurred in server process:')
|
|
sys.exit('Server error: %s' % e)
|
|
finally:
|
|
logging.info('Server process function exiting.')
|
|
logging.shutdown() # Flush and close handlers
|
|
|
|
logging.info('Daemon command finished normally after serving request.')
|
|
sys.exit(0) # Ensure clean exit
|