diff options
9 files changed, 361 insertions, 10 deletions
@@ -3,7 +3,7 @@ This repository is the collection of packages that are installed on the standard 1. [Image Bundle](https://cloud.google.com/compute/docs/images#buildingimage) - Tool that creates an image file out of a disk attached to a GCE VM. 1. [Google Startup Scripts](https://cloud.google.com/compute/docs/startupscript) - Scripts and configuration files that setup a Linux-based image to work smoothly with GCE. -1. Google Daemon - A service that manages user accounts, maintains ssh login keys, and syncs public endpoint IP addresses. +1. Google Daemon - A service that manages user accounts, maintains ssh login keys, syncs the system clock after migration, and syncs public endpoint IP addresses. ## Installation The easiest way to install these packages into a Linux-based image is to extract each tarball to `/` (root). Image Bundle does not have a directory structure, it is recommended to it extract to `/usr/share/imagebundle`. The tarballs are available in [releases](https://github.com/GoogleCloudPlatform/compute-image-packages/releases). diff --git a/google-daemon/etc/init.d/google-clock-sync-manager b/google-daemon/etc/init.d/google-clock-sync-manager new file mode 100755 index 0000000..b85f9de --- /dev/null +++ b/google-daemon/etc/init.d/google-clock-sync-manager @@ -0,0 +1,153 @@ +#! /bin/sh +# Copyright 2015 Google Inc. 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. +# +### BEGIN INIT INFO +# Provides: google-clock-manager +# Required-Start: $network $syslog +# Required-Stop: $network +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: Example initscript +# Description: This file should be used to construct scripts to be +# placed in /etc/init.d. +### END INIT INFO + +# Do NOT "set -e" + +# PATH should only include /usr/* if it runs after the mountnfs.sh script +PATH=/sbin:/usr/sbin:/bin:/usr/bin +DESC="Google clock sync manager" +NAME=google-clock-sync-manager +DAEMON=/usr/share/google/google_daemon/manage_clock_sync.py +DAEMON_ARGS="" +PIDFILE=/var/run/$NAME.pid +SCRIPTNAME=/etc/init.d/$NAME + +# Exit if the package is not installed +[ -x "$DAEMON" ] || exit 0 + +# Read configuration variable file if it is present +[ -r /etc/default/$NAME ] && . /etc/default/$NAME + +# Load the VERBOSE setting and other rcS variables +. /lib/init/vars.sh + +# Define LSB log_* functions. +# Depend on lsb-base (>= 3.2-14) to ensure that this file is present +# and status_of_proc is working. +. /lib/lsb/init-functions + +# If we're running under upstart, let the upstart config file handle things. +# Debian 7 and newer have a near-one-liner function to detect this... +if type init_is_upstart >/dev/null 2>&1; then + # ... which we can use if present. + init_is_upstart && exit 0 +else + # Otherwise, directly include the core line of Debian 7's version. + # Authorship credit: http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=661109 + if [ -x /sbin/initctl ] && /sbin/initctl version | /bin/grep -q upstart; then + exit 0 + fi +fi + +# +# Function that starts the daemon/service +# +do_start() +{ + # Return + # 0 if daemon has been started + # 1 if daemon was already running + # 2 if daemon could not be started + start-stop-daemon --start --quiet --make-pidfile --background \ + --pidfile $PIDFILE --exec $DAEMON --test > /dev/null \ + || return 1 + start-stop-daemon --start --quiet --make-pidfile --background \ + --pidfile $PIDFILE --exec $DAEMON -- \ + $DAEMON_ARGS || return 2 +} + +# +# Function that stops the daemon/service +# +do_stop() +{ + # Return + # 0 if daemon has been stopped + # 1 if daemon was already stopped + # 2 if daemon could not be stopped + # other if a failure occurred + start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE + RETVAL="$?" + [ "$RETVAL" = 2 ] && return 2 + # Wait for children to finish too if this is a daemon that forks + # and if the daemon is only ever run from this initscript. + # If the above conditions are not satisfied then add some other code + # that waits for the process to drop all resources that could be + # needed by services started subsequently. A last resort is to + # sleep for some time. + start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 \ + --pidfile $PIDFILE + [ "$?" = 2 ] && return 2 + # Many daemons don't delete their pidfiles when they exit. + rm -f $PIDFILE + return "$RETVAL" +} + +case "$1" in + start) + [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME" + do_start + case "$?" in + 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; + 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; + esac + ;; + stop) + [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME" + do_stop + case "$?" in + 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; + 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; + esac + ;; + status) + status_of_proc -p "$PIDFILE" "$DAEMON" "$NAME" && exit 0 || exit $? + ;; + restart|force-reload) + log_daemon_msg "Restarting $DESC" "$NAME" + do_stop + case "$?" in + 0|1) + do_start + case "$?" in + 0) log_end_msg 0 ;; + 1) log_end_msg 1 ;; # Old process is still running + *) log_end_msg 1 ;; # Failed to start + esac + ;; + *) + # Failed to stop + log_end_msg 1 + ;; + esac + ;; + *) + echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2 + exit 3 + ;; +esac + +: diff --git a/google-daemon/etc/init/google-clock-sync-manager.conf b/google-daemon/etc/init/google-clock-sync-manager.conf new file mode 100755 index 0000000..6d23a28 --- /dev/null +++ b/google-daemon/etc/init/google-clock-sync-manager.conf @@ -0,0 +1,5 @@ +# This service syncs the clock after migration in a Google Compute Engine instance. +start on google-rc-local-has-run + +respawn +exec /usr/share/google/google_daemon/manage_clock_sync.py diff --git a/google-daemon/usr/lib/systemd/system/google-clock-sync-manager.service b/google-daemon/usr/lib/systemd/system/google-clock-sync-manager.service new file mode 100644 index 0000000..c4fcc9b --- /dev/null +++ b/google-daemon/usr/lib/systemd/system/google-clock-sync-manager.service @@ -0,0 +1,11 @@ +[Unit] +Description=Google Compute Engine Clock Sync Daemon +After=network.target +Requires=network.target + +[Service] +Type=simple +ExecStart=/usr/share/google/google_daemon/manage_clock_sync.py + +[Install] +WantedBy=multi-user.target diff --git a/google-daemon/usr/share/google/google_daemon/accounts_manager.py b/google-daemon/usr/share/google/google_daemon/accounts_manager.py index 0e9897b..c1f2351 100644 --- a/google-daemon/usr/share/google/google_daemon/accounts_manager.py +++ b/google-daemon/usr/share/google/google_daemon/accounts_manager.py @@ -50,9 +50,9 @@ class AccountsManager(object): while True: # Fork and run the key regeneration and account update while the # parent waits for the subprocess to finish before continuing. - + # Create a pipe used to get the new etag value from child - reader, writer = os.pipe() # these are file descriptors, not file objects + reader, writer = os.pipe() # these are file descriptors, not file objects pid = os.fork() if pid: # we are the parent @@ -117,7 +117,7 @@ class AccountsManager(object): for entry in all_accounts if os.path.isfile(os.path.join(entry.pw_dir, keyfile_suffix))] extra_usernames = set(sshable_usernames) - set(desired_accounts.keys()) - + if desired_accounts: for username, ssh_keys in desired_accounts.iteritems(): if not username: diff --git a/google-daemon/usr/share/google/google_daemon/desired_accounts.py b/google-daemon/usr/share/google/google_daemon/desired_accounts.py index 9afe78c..feb3f14 100755 --- a/google-daemon/usr/share/google/google_daemon/desired_accounts.py +++ b/google-daemon/usr/share/google/google_daemon/desired_accounts.py @@ -87,7 +87,7 @@ def KeyHasExpired(key): expire_str) logging.error('Not expiring key.') return False - + # Expire the key if and only if we have exceeded the expiration timestamp. return (datetime.datetime.utcnow() > expire_time) @@ -170,7 +170,7 @@ class DesiredAccounts(object): return (attribute_value, etag) except urllib2.HTTPError as e: if e.code == 404: - # The attribute doesn't exist. Return None. + # The attribute doesn't exist. Return None. # No need to log a warning. return None # rethrow the exception since we don't know what it is. Let the @@ -231,6 +231,6 @@ class DesiredAccounts(object): logging.debug('Project sshKeys attribute not found.') # sshKeys doesn't exist for either project or instance. account_data = None - + self.attributes_etag = attributes_etag_cache return AccountDataToDictionary(account_data) diff --git a/google-daemon/usr/share/google/google_daemon/manage_accounts.py b/google-daemon/usr/share/google/google_daemon/manage_accounts.py index 0696ab2..7e18ae6 100755 --- a/google-daemon/usr/share/google/google_daemon/manage_accounts.py +++ b/google-daemon/usr/share/google/google_daemon/manage_accounts.py @@ -41,12 +41,12 @@ from utils import System def Main(accounts, desired_accounts, system, logger, log_handler, lock_file, lock_fname=None, single_pass=True, daemon_mode=False, debug_mode=False): - + if not log_handler: log_handler = system.MakeLoggingHandler( 'accounts-from-metadata', logging.handlers.SysLogHandler.LOG_AUTH) system.SetLoggingHandler(logger, log_handler) - + if debug_mode: system.EnableDebugLogging(logger) logging.debug('Running in Debug Mode') @@ -54,7 +54,7 @@ def Main(accounts, desired_accounts, system, logger, accounts_manager = AccountsManager( accounts, desired_accounts, system, lock_file, lock_fname, single_pass) - + if daemon_mode: manager_daemon = AccountsManagerDaemon(None, accounts_manager) manager_daemon.StartDaemon() diff --git a/google-daemon/usr/share/google/google_daemon/manage_clock_sync.py b/google-daemon/usr/share/google/google_daemon/manage_clock_sync.py new file mode 100755 index 0000000..c49f699 --- /dev/null +++ b/google-daemon/usr/share/google/google_daemon/manage_clock_sync.py @@ -0,0 +1,85 @@ +#!/usr/bin/python +# Copyright 2015 Google Inc. 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. + +"""Manages clock syncing after migration on GCE instances.""" + +import logging +import os +import sys + +def FixPath(): + parent_dir = os.path.dirname(os.path.realpath(sys.argv[0])) + if os.path.isdir(parent_dir): + sys.path.append(parent_dir) + + +FixPath() + +from utils import LockFile +from utils import System +from metadata_watcher import MetadataWatcher + + +LOCKFILE = '/var/lock/google-clock-sync.lock' + + +def HandleClockDriftToken(metadata_watcher, on_change): + """Watches for and responds to drift-token changes. + + Args: + metadata_watcher: a MetadataWatcher object. + on_change: a callable to call for any change. + """ + clock_drift_token_key = 'instance/virtual-clock/drift-token' + + def Handler(event): + on_change(event) + + metadata_watcher.WatchMetadataForever(clock_drift_token_key, + Handler, initial_value='') + + +def OnChange(event): + """Called when clock drift token changes. + + Args: + event: the new value of the drift token. + """ + system = System() + logging.info('Clock drift token has changed: %s', event) + logging.info('Syncing system time with hardware clock...') + result = system.RunCommand(['/sbin/hwclock', '--hctosys']) + if system.RunCommandFailed(result): + logging.error('Syncing system time failed.') + else: + logging.info('Synced system time with hardware clock.') + + +def Main(system=System(), logger=logging.getLogger(), log_handler=None, + lock_file=LockFile(), lock_fname=None): + if not log_handler: + log_handler = system.MakeLoggingHandler( + 'google-clock-sync', logging.handlers.SysLogHandler.LOG_SYSLOG) + system.SetLoggingHandler(logger, log_handler) + logging.info('Starting GCE clock sync') + + if not lock_fname: + lock_fname = LOCKFILE + watcher = MetadataWatcher() + lock_file.RunExclusively(lock_fname, HandleClockDriftToken(watcher, OnChange)) + + +if __name__ == '__main__': + Main() diff --git a/google-daemon/usr/share/google/google_daemon/metadata_watcher.py b/google-daemon/usr/share/google/google_daemon/metadata_watcher.py new file mode 100755 index 0000000..af0a90a --- /dev/null +++ b/google-daemon/usr/share/google/google_daemon/metadata_watcher.py @@ -0,0 +1,97 @@ +#!/usr/bin/python +# Copyright 2015 Google Inc. 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. + +import httplib +import time +import urllib +import urllib2 + + +METADATA_URL = 'http://metadata.google.internal/computeMetadata/v1/' + + +class Error(Exception): + pass + + +class UnexpectedStatusException(Error): + pass + + +class MetadataWatcher(object): + """Watches for changing metadata.""" + + def __init__(self, httplib_module=httplib, time_module=time, + urllib_module=urllib, urllib2_module=urllib2): + self.httplib = httplib_module + self.time = time_module + self.urllib = urllib_module + self.urllib2 = urllib2_module + + def WatchMetadataForever(self, metadata_key, handler, initial_value=None): + """Watches for a change in the value of metadata. + + Args: + metadata_key: The key identifying which metadata to watch for changes. + handler: A callable to call when the metadata value changes. Will be passed + a single parameter, the new value of the metadata. + initial_value: The expected initial value for the metadata. The handler will + not be called on the initial metadata request unless the value differs + from this. + + Raises: + UnexpectedStatusException: If the http request is unsuccessful for an + unexpected reason. + """ + params = { + 'wait_for_change': 'true', + 'last_etag': 0, + } + + value = initial_value + while True: + # start a hanging-GET request for metadata key. + url = '{base_url}{key}?{params}'.format( + base_url=METADATA_URL, + key=metadata_key, + params=self.urllib.urlencode(params) + ) + req = self.urllib2.Request(url, headers={'Metadata-Flavor': 'Google'}) + + try: + response = self.urllib2.urlopen(req) + content = response.read() + status = response.getcode() + except self.urllib2.HTTPError as e: + content = None + status = e.code + + if status == self.httplib.SERVICE_UNAVAILABLE: + self.time.sleep(1) + continue + elif status == self.httplib.OK: + # Extract new metadata value and latest etag. + new_value = content + headers = response.info() + params['last_etag'] = headers['ETag'] + else: + raise UnexpectedStatusException(status) + + # If the metadata value changed, call the appropriate handler. + if value == initial_value: + value = new_value + elif value != new_value: + value = new_value + handler(value) |