summaryrefslogtreecommitdiff
path: root/google-daemon/usr/share/google/google_daemon/accounts_manager.py
blob: 59327961c05922db348a12cbce7738c23d78bd6e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# Copyright 2013 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.

"""Main driver logic for managing accounts on GCE instances."""

import logging
import os
import pwd
import time

LOCKFILE = '/var/lock/manage-accounts.lock'


class AccountsManager(object):
  """Create accounts on a machine."""

  def __init__(self, accounts_module, desired_accounts, system, lock_file,
               lock_fname, single_pass=True):
    """Construct an AccountsFromMetadata with the given module injections."""
    if not lock_fname:
      lock_fname = LOCKFILE
    self.accounts = accounts_module
    self.desired_accounts = desired_accounts
    self.lock_file = lock_file
    self.lock_fname = lock_fname
    self.system = system
    self.single_pass = single_pass

  def Main(self):
    logging.debug('AccountsManager main loop')
    # If this is a one-shot execution, then this can be run normally.
    # Otherwise, run the actual operations in a subprocess so that any
    # errors don't kill the long-lived process.
    if self.single_pass:
      self.RegenerateKeysAndUpdateAccounts()
      return
    # Run this forever in a loop.
    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
      pid = os.fork()
      if pid:
        # We are the parent.
        os.close(writer)
        reader = os.fdopen(reader) # turn reader into a file object
        etag = reader.read()
        if etag:
          self.desired_accounts.etag = etag
        reader.close()
        logging.debug('New etag: %s', self.desired_accounts.etag)
        os.waitpid(pid, 0)
      else:
        # We are the child.
        os.close(reader)
        writer = os.fdopen(writer, 'w')
        try:
          self.RegenerateKeysAndUpdateAccounts()
        except Exception as e:
          logging.warning('error while trying to update accounts: %s', e)
          # An error happened while trying to update the accounts.
          # Sleep for five seconds before trying again.
          time.sleep(5)

        # Write the etag to pass to parent.
        etag = self.desired_accounts.etag or ''
        writer.write(etag)
        writer.close()

        # The use of os._exit here is recommended for subprocesses spawned
        # by forking to avoid issues with running the cleanup tasks that
        # sys.exit() runs by preventing issues from the cleanup being run
        # once by the subprocess and once by the parent process.
        os._exit(0)

  def RegenerateKeysAndUpdateAccounts(self):
    """Regenerate the keys and update accounts as needed."""
    logging.debug('RegenerateKeysAndUpdateAccounts')
    if self.system.IsExecutable('/usr/share/google/first-boot'):
      self.system.RunCommand('/usr/share/google/first-boot')

    self.lock_file.RunExclusively(self.lock_fname, self.UpdateAccounts)

  def UpdateAccounts(self):
    """Update all accounts that should be present or exist already."""

    # Note GetDesiredAccounts() returns a dict of username->sshKeys mappings.
    desired_accounts = self.desired_accounts.GetDesiredAccounts()

    # Plan a processing pass for extra accounts existing on the system with a
    # ~/.ssh/authorized_keys file, even if they're not otherwise in the metadata
    # server; this will only ever remove the last added-by-Google key from
    # accounts which were formerly in the metadata server but are no longer.
    all_accounts = pwd.getpwall()
    keyfile_suffix = os.path.join('.ssh', 'authorized_keys')
    sshable_usernames = [
        entry.pw_name
        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:
          continue

        self.accounts.UpdateUser(username, ssh_keys)

    for username in extra_usernames:
      # If a username is present in extra_usernames, it is no longer reflected
      # in the metadata server but has an authorized_keys file. Therefore, we
      # should pass the empty list for sshKeys to ensure that any Google-managed
      # keys are no longer authorized.
      self.accounts.UpdateUser(username, [])