def WriteComputerMSULog(uuid, details): """Write log details from MSU GUI into ComputerMSULog model. Args: uuid: str, computer uuid to update details: dict like = { 'event': str, 'something_happened', 'source': str, 'MSU' or 'user', 'user': str, 'username', 'time': int, epoch seconds, 'desc': str, 'additional descriptive text', } """ uuid = common.SanitizeUUID(uuid) key = '%s_%s_%s' % (uuid, details['source'], details['event']) c = models.ComputerMSULog(key_name=key) c.uuid = uuid c.event = details['event'] c.source = details['source'] c.user = details['user'] c.desc = details['desc'] try: mtime = util.Datetime.utcfromtimestamp(details.get('time', None)) except ValueError, e: logging.warning('Ignoring msu_log time; %s' % str(e)) mtime = datetime.datetime.utcnow()
def WriteBrokenClient(uuid, reason, details): """Saves a BrokenClient entity to Datastore for the given UUID. Args: uuid: str, uuid of client. reason: str, short description of broken state the client is reporting. details: str, details or debugging output of the broken report. """ # If the details string contains facter output, parse it. facts = {} lines = details.splitlines() for line in lines: try: (key, unused_sep, value) = line.split(' ', 2) except ValueError: continue # current line was not facter, continue. value = value.strip() facts[key] = value # Update the existing, or create a new ComputerClientBroken entity. uuid = common.SanitizeUUID(uuid) bc = models.ComputerClientBroken.get_or_insert(uuid) bc.broken_datetimes.append(datetime.datetime.utcnow()) bc.reason = reason bc.details = details bc.fixed = False # Previously fixed computers will show up again. bc.hostname = facts.get('hostname', '') bc.owner = facts.get('primary_user', '') bc.serial = facts.get('sp_serial_number', '') bc.uuid = uuid bc.put()
def WriteClientLog(model, uuid, **kwargs): """Writes a ClientLog entry. Args: model: db.Model to write to. uuid: str uuid of client. kwargs: property/value pairs to write to the model; uuid not allowed. Returns: models.Computer instance which is this client """ if 'uuid' in kwargs: #logging.debug('WriteClientLog: Deleting uuid from kwargs') del(kwargs['uuid']) uuid = common.SanitizeUUID(uuid) if 'computer' not in kwargs: kwargs['computer'] = models.Computer.get_by_key_name(uuid) l = model(uuid=uuid, **kwargs) try: l.put() except (db.Error, apiproxy_errors.Error, runtime.DeadlineExceededError): logging.warning('WriteClientLog put() failure; deferring...') now_str = datetime.datetime.utcnow().strftime('%Y-%m-%d-%H-%M-%S') deferred_name = 'write-client-log-%s-%s' % (uuid, now_str) deferred.defer( WriteClientLog, model, uuid, _name=deferred_name, _countdown=5, **kwargs) return kwargs['computer']
def ParseClientId(client_id, uuid=None): """Splits a client id string and converts all key/value pairs to a dict. Also, this truncates string values to 500 characters. Args: client_id: string client id with "|" as delimiter. uuid: optional string uuid to override the uuid in client_id. Returns: Dict. Client id string "foo=bar|key=|one=1" yields {'foo': 'bar', 'key': None, 'one': '1'}. """ if client_id and client_id.find('\n') > -1: logging.warning('ParseClientId: client_id has newline: %s', base64.b64encode(client_id)) client_id = client_id.replace('\n', '_') # Convert str input to unicode. if type(client_id) is str: try: client_id = client_id.decode('utf-8') except UnicodeDecodeError: client_id = client_id.decode('utf-8', 'replace') logging.warning('UnicodeDecodeError on client_id: %s', client_id) out = KeyValueStringToDict(client_id) # If any required fields were not present in the client id string, add them. # Also cast all values to their defined output types. for field, value_type in CLIENT_ID_FIELDS.iteritems(): if field not in out or out[field] is None: out[field] = None elif value_type is bool: out[field] = GetBoolValueFromString(out[field]) elif value_type is str: # truncate str fields to 500 characters, the StringProperty limit. out[field] = out[field][:500] else: try: out[field] = value_type(out[field]) except ValueError: logging.warning( 'Error casting client id %s to defined type: %s', field, out[field]) out[field] = None if out['track'] not in common.TRACKS: if out['track'] is not None: logging.warning('Invalid track requested: %s', out['track']) out['track'] = common.DEFAULT_TRACK if uuid: out['uuid'] = common.SanitizeUUID(uuid) return out
def put(self, file_type=None, file_name=None): """UploadFile PUT handler. Returns: A webapp.Response() response. """ session = gaeserver.DoMunkiAuth() uuid = main_common.SanitizeUUID(session.uuid) if not file_type or not file_name: logging.warning('file_type=%s , file_name=%s', file_type, file_name) self.error(httplib.NOT_FOUND) return if file_type == 'log': key = '%s_%s' % (uuid, file_name) l = models.ClientLogFile(key_name=key) l.log_file = self.request.body l.uuid = uuid l.name = file_name try: l.put() except (apiproxy_errors.RequestTooLargeError, datastore_errors.BadRequestError): # detastore raises BadRequestError now, if file too large. logging.warning( 'UploadFile may be log too large; truncating...') # Datastore has a 1MB entity limit and models.ClientLogFile.log_file # uses zlib compression. Anecdotal evidence of a handlful of log files # over 8MB in size compress down to well under 1MB. Therefore, slice # the top of the log data off at a conversative max, before retrying the # Datastore put. max_log_size_bytes = 5 * 1024 * 1024 l.log_file = ( '*** Log truncated by Simian due to size ***\n\n' + self.request.body[-1 * max_log_size_bytes:]) l.put() c = models.Computer.get_by_key_name(uuid) recipients = c.upload_logs_and_notify c.upload_logs_and_notify = None c.put() # c.upload_logs_and_notify may be None from a previous upload, as multiple # files may be uploaded in different requests per execution. if recipients: recipients = recipients.split(',') deferred.defer(SendNotificationEmail, recipients, c, settings.SERVER_HOSTNAME) else: self.error(httplib.NOT_FOUND)
def post(self): """Reports get handler. Returns: A webapp.Response() response. """ session = gaeserver.DoMunkiAuth() uuid = main_common.SanitizeUUID(session.uuid) report_type = self.request.get('_report_type') feedback_requested = self.request.get('_feedback') message = None details = None client_id = None computer = None if report_type == 'preflight' or report_type == 'postflight': client_id_str = urllib.unquote(self.request.get('client_id')) client_id = common.ParseClientId(client_id_str, uuid=uuid) user_settings_str = self.request.get('user_settings') user_settings = None try: if user_settings_str: user_settings = util.Deserialize( urllib.unquote(str(user_settings_str))) except util.DeserializeError: logging.warning('Client %s sent broken user_settings: %s', client_id_str, user_settings_str) pkgs_to_install = self.request.get_all('pkgs_to_install') apple_updates_to_install = self.request.get_all( 'apple_updates_to_install') computer = models.Computer.get_by_key_name(uuid) ip_address = os.environ.get('REMOTE_ADDR', '') report_feedback = None if report_type == 'preflight': # if the UUID is known to be lost/stolen, log this connection. if models.ComputerLostStolen.IsLostStolen(uuid): logging.warning('Connection from lost/stolen machine: %s', uuid) models.ComputerLostStolen.LogLostStolenConnection( computer=computer, ip_address=ip_address) # we want to get feedback now, before preflight_datetime changes. if feedback_requested: client_exit = self.request.get('client_exit', None) report_feedback = self.GetReportFeedback( uuid, report_type, computer=computer, ip_address=ip_address, client_exit=client_exit) self.response.out.write(report_feedback) # if report feedback calls for a client exit, log it. if report_feedback == common.ReportFeedback.EXIT: if not client_exit: # client didn't ask for an exit, which means server decided. client_exit = 'Connection from defined exit IP address' common.WriteClientLog(models.PreflightExitLog, uuid, computer=computer, exit_reason=client_exit) common.LogClientConnection(report_type, client_id, user_settings, pkgs_to_install, apple_updates_to_install, computer=computer, ip_address=ip_address, report_feedback=report_feedback) elif report_type == 'install_report': computer = models.Computer.get_by_key_name(uuid) self._LogInstalls(self.request.get_all('installs'), computer) for removal in self.request.get_all('removals'): common.WriteClientLog(models.ClientLog, uuid, computer=computer, action='removal', details=removal) for problem in self.request.get_all('problem_installs'): common.WriteClientLog(models.ClientLog, uuid, computer=computer, action='install_problem', details=problem) elif report_type == 'preflight_exit': # NOTE(user): only remains for older clients. message = self.request.get('message') computer = common.WriteClientLog(models.PreflightExitLog, uuid, exit_reason=message) elif report_type == 'broken_client': # Default reason of "objc" to support legacy clients, existing when objc # was the only broken state ever reported. reason = self.request.get('reason', 'objc') details = self.request.get('details') logging.warning('Broken Munki client (%s): %s', reason, details) common.WriteBrokenClient(uuid, reason, details) elif report_type == 'msu_log': details = {} for k in ['time', 'user', 'source', 'event', 'desc']: details[k] = self.request.get(k, None) common.WriteComputerMSULog(uuid, details) else: # unknown report type; log all post params. params = [] for param in self.request.arguments(): params.append('%s=%s' % (param, self.request.get_all(param))) common.WriteClientLog(models.ClientLog, uuid, action='unknown', details=str(params)) # If the client asked for feedback, get feedback and respond. # Skip this if the report_type is preflight, as report feedback was # retrieved before LogComputerConnection changed preflight_datetime. if feedback_requested and report_type != 'preflight': self.response.out.write( self.GetReportFeedback( uuid, report_type, message=message, details=details, computer=computer, ))
def post(self): """Reports get handler. Returns: A webapp.Response() response. """ session = gaeserver.DoMunkiAuth() uuid = main_common.SanitizeUUID(session.uuid) report_type = self.request.get('_report_type') report_feedback = {} message = None details = None client_id = None computer = None if report_type == 'preflight' or report_type == 'postflight': client_id_str = urllib.unquote(self.request.get('client_id')) client_id = common.ParseClientId(client_id_str, uuid=uuid) user_settings_str = self.request.get('user_settings') user_settings = None try: if user_settings_str: user_settings = util.Deserialize( urllib.unquote(str(user_settings_str))) except util.DeserializeError: logging.warning('Client %s sent broken user_settings: %s', client_id_str, user_settings_str) pkgs_to_install = self.request.get_all('pkgs_to_install') apple_updates_to_install = self.request.get_all( 'apple_updates_to_install') computer = models.Computer.get_by_key_name(uuid) ip_address = os.environ.get('REMOTE_ADDR', '') if report_type == 'preflight': # we want to get feedback now, before preflight_datetime changes. client_exit = self.request.get('client_exit', None) report_feedback = self.GetReportFeedback( uuid, report_type, computer=computer, ip_address=ip_address, client_exit=client_exit) if self.request.get('json') == '1': self.response.out.write(JSON_PREFIX + json.dumps(report_feedback)) else: # For legacy clients that accept a single string, not JSON. feedback_to_send = 'OK' for feedback in LEGACY_FEEDBACK_LIST: if report_feedback.get(feedback.lower()): feedback_to_send = feedback self.response.out.write(feedback_to_send) # if report feedback calls for a client exit, log it. if report_feedback.get('exit'): if not client_exit: # client didn't ask for an exit, which means server decided. client_exit = 'Connection from defined exit IP address' common.WriteClientLog(models.PreflightExitLog, uuid, computer=computer, exit_reason=client_exit) common.LogClientConnection(report_type, client_id, user_settings, pkgs_to_install, apple_updates_to_install, computer=computer, ip_address=ip_address, report_feedback=report_feedback) elif report_type == 'install_report': computer = models.Computer.get_by_key_name(uuid) self._LogInstalls(self.request.get_all('installs'), computer) for removal in self.request.get_all('removals'): common.WriteClientLog(models.ClientLog, uuid, computer=computer, action='removal', details=removal) for problem in self.request.get_all('problem_installs'): common.WriteClientLog(models.ClientLog, uuid, computer=computer, action='install_problem', details=problem) elif report_type == 'broken_client': # Default reason of "objc" to support legacy clients, existing when objc # was the only broken state ever reported. reason = self.request.get('reason', 'objc') details = self.request.get('details') logging.warning('Broken Munki client (%s): %s', reason, details) common.WriteBrokenClient(uuid, reason, details) elif report_type == 'msu_log': details = {} for k in ['time', 'user', 'source', 'event', 'desc']: details[k] = self.request.get(k, None) common.WriteComputerMSULog(uuid, details) else: # unknown report type; log all post params. params = [] for param in self.request.arguments(): params.append('%s=%s' % (param, self.request.get_all(param))) common.WriteClientLog(models.ClientLog, uuid, action='unknown', details=str(params))