Utils/handlerutil2.py (277 lines of code) (raw):

# # Handler library for Linux IaaS # # Copyright 2014 Microsoft Corporation # # 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. """ JSON def: HandlerEnvironment.json [{ "name": "ExampleHandlerLinux", "seqNo": "seqNo", "version": "1.0", "handlerEnvironment": { "logFolder": "<your log folder location>", "configFolder": "<your config folder location>", "statusFolder": "<your status folder location>", "heartbeatFile": "<your heartbeat file location>", } }] Example ./config/1.settings "{"runtimeSettings":[{"handlerSettings":{"protectedSettingsCertThumbprint":"1BE9A13AA1321C7C515EF109746998BAB6D86FD1","protectedSettings": "MIIByAYJKoZIhvcNAQcDoIIBuTCCAbUCAQAxggFxMIIBbQIBADBVMEExPzA9BgoJkiaJk/IsZAEZFi9XaW5kb3dzIEF6dXJlIFNlcnZpY2UgTWFuYWdlbWVudCBmb3IgR+nhc6VHQTQpCiiV2zANBgkqhkiG9w0BAQEFAASCAQCKr09QKMGhwYe+O4/a8td+vpB4eTR+BQso84cV5KCAnD6iUIMcSYTrn9aveY6v6ykRLEw8GRKfri2d6tvVDggUrBqDwIgzejGTlCstcMJItWa8Je8gHZVSDfoN80AEOTws9Fp+wNXAbSuMJNb8EnpkpvigAWU2v6pGLEFvSKC0MCjDTkjpjqciGMcbe/r85RG3Zo21HLl0xNOpjDs/qqikc/ri43Y76E/Xv1vBSHEGMFprPy/Hwo3PqZCnulcbVzNnaXN3qi/kxV897xGMPPC3IrO7Nc++AT9qRLFI0841JLcLTlnoVG1okPzK9w6ttksDQmKBSHt3mfYV+skqs+EOMDsGCSqGSIb3DQEHATAUBggqhkiG9w0DBwQITgu0Nu3iFPuAGD6/QzKdtrnCI5425fIUy7LtpXJGmpWDUA==","publicSettings":{"port":"3000"}}}]}" Example HeartBeat { "version": 1.0, "heartbeat" : { "status": "ready", "code": 0, "Message": "Sample Handler running. Waiting for a new configuration from user." } } Example Status Report: [{"version":"1.0","timestampUTC":"2014-05-29T04:20:13Z","status":{"name":"Chef Extension Handler","operation":"chef-client-run","status":"success","code":0,"formattedMessage":{"lang":"en-US","message":"Chef-client run success"}}}] """ import os import os.path import sys import base64 import json import time import re import Utils.extensionutils as ext_utils import Utils.constants as constants import Utils.logger as logger from xml.etree import ElementTree from os.path import join DateTimeFormat = "%Y-%m-%dT%H:%M:%SZ" MANIFEST_XML = "manifest.xml" class HandlerContext: def __init__(self, name): self._name = name self._version = '0.0' self._config_dir = None self._log_dir = None self._log_file = None self._status_dir = None self._heartbeat_file = None self._seq_no = -1 self._status_file = None self._settings_file = None self._config = None return class HandlerUtility: def __init__(self, s_name=None, l_name=None, extension_version=None, logFileName='extension.log', console_logger=None, file_logger=None): self._log = logger.log self._log_to_con = console_logger self._log_to_file = file_logger self._error = logger.error self._logFileName = logFileName if s_name is None or l_name is None or extension_version is None: (l_name, s_name, extension_version) = self._get_extension_info() self._short_name = s_name self._extension_version = extension_version self._log_prefix = '[%s-%s] ' % (l_name, extension_version) def get_extension_version(self): return self._extension_version def _get_log_prefix(self): return self._log_prefix def _get_extension_info(self): if os.path.isfile(MANIFEST_XML): return self._get_extension_info_manifest() ext_dir = os.path.basename(os.getcwd()) (long_name, version) = ext_dir.split('-') short_name = long_name.split('.')[-1] return long_name, short_name, version def _get_extension_info_manifest(self): with open(MANIFEST_XML) as fh: doc = ElementTree.parse(fh) namespace = doc.find('{http://schemas.microsoft.com/windowsazure}ProviderNameSpace').text short_name = doc.find('{http://schemas.microsoft.com/windowsazure}Type').text version = doc.find('{http://schemas.microsoft.com/windowsazure}Version').text long_name = "%s.%s" % (namespace, short_name) return (long_name, short_name, version) def _get_current_seq_no(self, config_folder): seq_no = -1 cur_seq_no = -1 freshest_time = None for subdir, dirs, files in os.walk(config_folder): for file in files: try: cur_seq_no = int(os.path.basename(file).split('.')[0]) if (freshest_time == None): freshest_time = os.path.getmtime(join(config_folder, file)) seq_no = cur_seq_no else: current_file_m_time = os.path.getmtime(join(config_folder, file)) if (current_file_m_time > freshest_time): freshest_time = current_file_m_time seq_no = cur_seq_no except ValueError: continue return seq_no def log(self, message): self._log(self._get_log_prefix() + message) def log_to_console(self, message): if self._log_to_con is not None: self._log_to_con(self._get_log_prefix() + message) else: self.error("Unable to log to console, console log method not set") def log_to_file(self, message): if self._log_to_file is not None: self._log_to_file(self._get_log_prefix() + message) else: self.error("Unable to log to file, file log method not set") def error(self, message): self._error(self._get_log_prefix() + message) @staticmethod def redact_protected_settings(content): redacted_tmp = re.sub('"protectedSettings":\s*"[^"]+=="', '"protectedSettings": "*** REDACTED ***"', content) redacted = re.sub('"protectedSettingsCertThumbprint":\s*"[^"]+"', '"protectedSettingsCertThumbprint": "*** REDACTED ***"', redacted_tmp) return redacted def _parse_config(self, ctxt): config = None try: config = json.loads(ctxt) except: self.error('JSON exception decoding ' + HandlerUtility.redact_protected_settings(ctxt)) if config is None: self.error("JSON error processing settings file:" + HandlerUtility.redact_protected_settings(ctxt)) else: handlerSettings = config['runtimeSettings'][0]['handlerSettings'] if 'protectedSettings' in handlerSettings and \ 'protectedSettingsCertThumbprint' in handlerSettings and \ handlerSettings['protectedSettings'] is not None and \ handlerSettings["protectedSettingsCertThumbprint"] is not None: protectedSettings = handlerSettings['protectedSettings'] thumb = handlerSettings['protectedSettingsCertThumbprint'] cert = constants.LibDir + '/' + thumb + '.crt' pkey = constants.LibDir + '/' + thumb + '.prv' unencodedSettings = base64.standard_b64decode(protectedSettings) openSSLcmd_cms = ['openssl', 'cms', '-inform', 'DER', '-decrypt', '-recip' , cert, '-inkey', pkey] cleartxt = ext_utils.run_send_stdin(openSSLcmd_cms, unencodedSettings)[1] if cleartxt is None: self.log("OpenSSL decode error using cms command with thumbprint " + thumb + "\n trying smime command") openSSLcmd_smime = ['openssl', 'smime', '-inform', 'DER', '-decrypt', '-recip' , cert, '-inkey', pkey] cleartxt = ext_utils.run_send_stdin(openSSLcmd_smime, unencodedSettings)[1] if cleartxt is None: self.error("OpenSSL decode error using smime command with thumbprint " + thumb) self.do_exit(1, "Enable", 'error', '1', 'Failed to decrypt protectedSettings') jctxt = '' try: jctxt = json.loads(cleartxt) except: self.error('JSON exception decoding ' + HandlerUtility.redact_protected_settings(cleartxt)) handlerSettings['protectedSettings']=jctxt self.log('Config decoded correctly.') return config def do_parse_context(self, operation): _context = self.try_parse_context() if not _context: self.do_exit(1, operation, 'error', '1', operation + ' Failed') return _context def try_parse_context(self): self._context = HandlerContext(self._short_name) handler_env = None config = None ctxt = None code = 0 # get the HandlerEnvironment.json. According to the extension handler spec, it is always in the ./ directory self.log('cwd is ' + os.path.realpath(os.path.curdir)) handler_env_file = './HandlerEnvironment.json' if not os.path.isfile(handler_env_file): self.error("Unable to locate " + handler_env_file) return None ctxt = ext_utils.get_file_contents(handler_env_file) if ctxt == None: self.error("Unable to read " + handler_env_file) try: handler_env = json.loads(ctxt) except: pass if handler_env == None: self.log("JSON error processing " + handler_env_file) return None if type(handler_env) == list: handler_env = handler_env[0] self._context._name = handler_env['name'] self._context._version = str(handler_env['version']) self._context._config_dir = handler_env['handlerEnvironment']['configFolder'] self._context._log_dir = handler_env['handlerEnvironment']['logFolder'] self._context._log_file = os.path.join(handler_env['handlerEnvironment']['logFolder'], self._logFileName) self._change_log_file() self._context._status_dir = handler_env['handlerEnvironment']['statusFolder'] self._context._heartbeat_file = handler_env['handlerEnvironment']['heartbeatFile'] self._context._seq_no = self._get_current_seq_no(self._context._config_dir) if self._context._seq_no < 0: self.error("Unable to locate a .settings file!") return None self._context._seq_no = str(self._context._seq_no) self.log('sequence number is ' + self._context._seq_no) self._context._status_file = os.path.join(self._context._status_dir, self._context._seq_no + '.status') self._context._settings_file = os.path.join(self._context._config_dir, self._context._seq_no + '.settings') self.log("setting file path is" + self._context._settings_file) ctxt = None ctxt = ext_utils.get_file_contents(self._context._settings_file) if ctxt == None: error_msg = 'Unable to read ' + self._context._settings_file + '. ' self.error(error_msg) return None self.log("JSON config: " + HandlerUtility.redact_protected_settings(ctxt)) self._context._config = self._parse_config(ctxt) return self._context def _change_log_file(self): self.log("Change log file to " + self._context._log_file) # this will change the logging file for all python files that share the same process logger.global_shared_context_logger = logger.Logger(self._context._log_file, '/dev/stdout') def is_seq_smaller(self): return int(self._context._seq_no) <= self._get_most_recent_seq() def save_seq(self): self._set_most_recent_seq(self._context._seq_no) self.log("set most recent sequence number to " + str(self._context._seq_no)) def exit_if_enabled(self, remove_protected_settings=False): self.exit_if_seq_smaller(remove_protected_settings) def exit_if_seq_smaller(self, remove_protected_settings): if(self.is_seq_smaller()): self.log( "Current sequence number, " + str(self._context._seq_no) + ", is not greater than the sequence number of the most recent executed configuration. Exiting...") sys.exit(0) self.save_seq() if remove_protected_settings: self.scrub_settings_file() def _get_most_recent_seq(self): if (os.path.isfile('mrseq')): seq = ext_utils.get_file_contents('mrseq') if (seq): return int(seq) return -1 def is_current_config_seq_greater_inused(self): return int(self._context._seq_no) > self._get_most_recent_seq() def get_inused_config_seq(self): return self._get_most_recent_seq() def set_inused_config_seq(self, seq): self._set_most_recent_seq(seq) def _set_most_recent_seq(self, seq): ext_utils.set_file_contents('mrseq', str(seq)) def do_status_report(self, operation, status, status_code, message): self.log("{0},{1},{2},{3}".format(operation, status, status_code, message)) tstamp = time.strftime(DateTimeFormat, time.gmtime()) stat = [{ "version": self._context._version, "timestampUTC": tstamp, "status": { "name": self._context._name, "operation": operation, "status": status, "code": status_code, "formattedMessage": { "lang": "en-US", "message": message } } }] stat_rept = json.dumps(stat) if self._context._status_file: tmp = "%s.tmp" % (self._context._status_file) with open(tmp, 'w+') as f: f.write(stat_rept) os.rename(tmp, self._context._status_file) def do_heartbeat_report(self, heartbeat_file, status, code, message): # heartbeat health_report = '[{"version":"1.0","heartbeat":{"status":"' + status + '","code":"' + code + '","Message":"' + message + '"}}]' if ext_utils.set_file_contents(heartbeat_file, health_report) is None: self.error('Unable to wite heartbeat info to ' + heartbeat_file) def do_exit(self, exit_code, operation, status, code, message): try: self.do_status_report(operation, status, code, message) except Exception as e: self.log("Can't update status: " + str(e)) sys.exit(exit_code) def get_name(self): return self._context._name def get_seq_no(self): return self._context._seq_no def get_log_dir(self): return self._context._log_dir def get_handler_settings(self): if (self._context._config != None): return self._context._config['runtimeSettings'][0]['handlerSettings'] return None def get_protected_settings(self): if (self._context._config != None): return self.get_handler_settings().get('protectedSettings') return None def get_public_settings(self): handlerSettings = self.get_handler_settings() if (handlerSettings != None): return self.get_handler_settings().get('publicSettings') return None def scrub_settings_file(self): content = ext_utils.get_file_contents(self._context._settings_file) redacted = HandlerUtility.redact_protected_settings(content) ext_utils.set_file_contents(self._context._settings_file, redacted)