VMAccess/vmaccess.py (462 lines of code) (raw):

#!/usr/bin/env python # # VMAccess extension # # 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. import os import re import shutil import sys import tempfile import time import traceback import Utils.handlerutil2 as handler_util import Utils.logger as logger import Utils.extensionutils as ext_utils import Utils.distroutils as dist_utils import Utils.constants as constants import Utils.ovfutils as ovf_utils # Define global variables ExtensionShortName = 'VMAccess' BeginCertificateTag = '-----BEGIN CERTIFICATE-----' EndCertificateTag = '-----END CERTIFICATE-----' BeginSSHTag = '---- BEGIN SSH2 PUBLIC KEY ----' OutputSplitter = ';' SshdConfigPath = '/etc/ssh/sshd_config' SshdConfigBackupPath = '/var/cache/vmaccess/backup' # overwrite the default logger logger.global_shared_context_logger = logger.Logger('/var/log/waagent.log', '/dev/stdout') def get_os_name(): if os.path.isfile(constants.os_release): return ext_utils.get_line_starting_with("NAME", constants.os_release) elif os.path.isfile(constants.system_release): return ext_utils.get_file_contents(constants.system_release) return None def get_linux_agent_conf_filename(os_name): if os_name is not None: if re.search("coreos", os_name, re.IGNORECASE) or re.search("flatcar", os_name, re.IGNORECASE): return "/usr/share/oem/waagent.conf" return "/etc/waagent.conf" class ConfigurationProvider(object): """ Parse amd store key:values in waagent.conf """ def __init__(self, wala_config_file): self.values = dict() if not os.path.isfile(wala_config_file): logger.warning("Missing configuration in {0}, setting default values for PasswordCryptId and PasswordCryptSaltLength".format(wala_config_file)) self.values["Provisioning.PasswordCryptId"] = "6" self.values["Provisioning.PasswordCryptSaltLength"] = 10 return try: for line in ext_utils.get_file_contents(wala_config_file).split('\n'): if not line.startswith("#") and "=" in line: parts = line.split()[0].split('=') value = parts[1].strip("\" ") if value != "None": self.values[parts[0]] = value else: self.values[parts[0]] = None # when get_file_contents returns none except AttributeError: logger.error("Unable to parse {0}".format(wala_config_file)) raise return def get(self, key): return self.values.get(key) def yes(self, key): config_value = self.get(key) if config_value is not None and config_value.lower().startswith("y"): return True else: return False def no(self, key): config_value = self.get(key) if config_value is not None and config_value.lower().startswith("n"): return True else: return False OSName = get_os_name() Configuration = ConfigurationProvider(get_linux_agent_conf_filename(OSName)) MyDistro = dist_utils.get_my_distro(Configuration, OSName) def main(): logger.log("%s started to handle." % ExtensionShortName) try: for a in sys.argv[1:]: if re.match("^([-/]*)(disable)", a): disable() elif re.match("^([-/]*)(uninstall)", a): uninstall() elif re.match("^([-/]*)(install)", a): install() elif re.match("^([-/]*)(enable)", a): enable() elif re.match("^([-/]*)(update)", a): update() except Exception as e: err_msg = "Failed with error: {0}, {1}".format(e, traceback.format_exc()) logger.error(err_msg) def install(): hutil = handler_util.HandlerUtility() hutil.do_parse_context('Install') hutil.do_exit(0, 'Install', 'success', '0', 'Install Succeeded') def enable(): hutil = handler_util.HandlerUtility() hutil.do_parse_context('Enable') try: hutil.exit_if_enabled(remove_protected_settings=True) # If no new seqNum received, exit. reset_ssh = None remove_user = None restore_backup_ssh = None protect_settings = hutil.get_protected_settings() if protect_settings: reset_ssh = protect_settings.get('reset_ssh', False) remove_user = protect_settings.get('remove_user') restore_backup_ssh = protect_settings.get('restore_backup_ssh', False) if remove_user and _is_sshd_config_modified(protect_settings): ext_utils.add_extension_event(name=hutil.get_name(), op=constants.WALAEventOperation.Enable, is_success=False, message="(03002)Argument error, conflicting operations") raise Exception("Cannot reset sshd_config and remove a user in one operation.") _forcibly_reset_chap(hutil) if reset_ssh or restore_backup_ssh: _open_ssh_port() hutil.log("Succeeded in check and open ssh port.") ext_utils.add_extension_event(name=hutil.get_name(), op="scenario", is_success=True, message="reset-ssh") _reset_sshd_config(hutil, restore_backup_ssh) hutil.log("Succeeded in {0} sshd_config.".format("resetting" if reset_ssh else "restoring")) if remove_user: ext_utils.add_extension_event(name=hutil.get_name(), op="scenario", is_success=True, message="remove-user") _remove_user_account(remove_user, hutil) _set_user_account_pub_key(protect_settings, hutil) if _is_sshd_config_modified(protect_settings): MyDistro.restart_ssh_service() check_and_repair_disk(hutil) hutil.do_exit(0, 'Enable', 'success', '0', 'Enable succeeded.') except Exception as e: hutil.error(("Failed to enable the extension with error: {0}, " "stack trace: {1}").format(str(e), traceback.format_exc())) hutil.do_exit(1, 'Enable', 'error', '0', "Enable failed: {0}".format(str(e))) def _forcibly_reset_chap(hutil): name = "ChallengeResponseAuthentication" _backup_and_update_sshd_config(hutil, name, "no") MyDistro.restart_ssh_service() def _is_sshd_config_modified(protected_settings): result = protected_settings.get('reset_ssh') or protected_settings.get('restore_backup_ssh') or protected_settings.get('password') return result is not None def uninstall(): hutil = handler_util.HandlerUtility() hutil.do_parse_context('Uninstall') hutil.do_exit(0, 'Uninstall', 'success', '0', 'Uninstall succeeded') def disable(): hutil = handler_util.HandlerUtility() hutil.do_parse_context('Disable') hutil.do_exit(0, 'Disable', 'success', '0', 'Disable Succeeded') def update(): hutil = handler_util.HandlerUtility() hutil.do_parse_context('Update') hutil.do_exit(0, 'Update', 'success', '0', 'Update Succeeded') def _remove_user_account(user_name, hutil): hutil.log("Removing user account") try: sudoers = _get_other_sudoers(user_name) MyDistro.delete_account(user_name) _save_other_sudoers(sudoers) except Exception as e: ext_utils.add_extension_event(name=hutil.get_name(), op=constants.WALAEventOperation.Enable, is_success=False, message="(02102)Failed to remove user.") raise Exception("Failed to remove user {0}".format(e)) ext_utils.add_extension_event(name=hutil.get_name(), op=constants.WALAEventOperation.Enable, is_success=True, message="Successfully removed user") def _set_user_account_pub_key(protect_settings, hutil): ovf_env = None try: ovf_xml = ext_utils.get_file_contents('/var/lib/waagent/ovf-env.xml') if ovf_xml is not None: ovf_env = ovf_utils.OvfEnv.parse(ovf_xml, Configuration, False, False) except (EnvironmentError, ValueError, KeyError, AttributeError, TypeError): pass if ovf_env is None: # default ovf_env with empty data ovf_env = ovf_utils.OvfEnv() logger.log("could not load ovf-env.xml") # user name must be provided if set ssh key or password if not protect_settings or 'username' not in protect_settings: return user_name = protect_settings['username'] user_pass = protect_settings.get('password') cert_txt = protect_settings.get('ssh_key') expiration = protect_settings.get('expiration') remove_prior_keys = protect_settings.get('remove_prior_keys') enable_passwordless_access = protect_settings.get('enable_passwordless_access', False) no_convert = False if not user_pass and not cert_txt and not ovf_env.SshPublicKeys: raise Exception("No password or ssh_key is specified.") if user_pass is not None and len(user_pass) == 0: user_pass = None hutil.log("empty passwords are not allowed, ignoring password reset") # Reset user account and password, password could be empty sudoers = _get_other_sudoers(user_name) error_string = MyDistro.create_account( user_name, user_pass, expiration, None, enable_passwordless_access) _save_other_sudoers(sudoers) if error_string is not None: err_msg = "Failed to create the account or set the password" ext_utils.add_extension_event(name=hutil.get_name(), op=constants.WALAEventOperation.Enable, is_success=False, message="(02101)" + err_msg) raise Exception(err_msg + " with " + error_string) hutil.log("Succeeded in creating the account or setting the password.") # Allow password authentication if user_pass is provided if user_pass is not None: ext_utils.add_extension_event(name=hutil.get_name(), op="scenario", is_success=True, message="create-user-with-password") _allow_password_auth(hutil) # Reset ssh key with the new public key passed in or reuse old public key. if cert_txt: # support for SSH2-compatible format for public keys in addition to OpenSSH-compatible format if cert_txt.strip().startswith(BeginSSHTag): ext_utils.set_file_contents("temp.pub", cert_txt.strip()) retcode, output = ext_utils.run_command_get_output(['ssh-keygen', '-i', '-f', 'temp.pub']) if retcode > 0: raise Exception("Failed to convert SSH2 key to OpenSSH key.") hutil.log("Succeeded in converting SSH2 key to OpenSSH key.") cert_txt = output os.remove("temp.pub") if cert_txt.strip().lower().startswith("ssh-rsa") or cert_txt.strip().lower().startswith("ssh-ed25519"): no_convert = True try: pub_path = os.path.join('/home/', user_name, '.ssh', 'authorized_keys') ovf_env.UserName = user_name if no_convert: if cert_txt: pub_path = ovf_env.prepare_dir(pub_path, MyDistro) final_cert_txt = cert_txt if not cert_txt.endswith("\n"): final_cert_txt = final_cert_txt + "\n" if remove_prior_keys == True: ext_utils.set_file_contents(pub_path, final_cert_txt) hutil.log("Removed prior ssh keys and added new key for user %s" % user_name) else: ext_utils.append_file_contents(pub_path, final_cert_txt) MyDistro.set_se_linux_context( pub_path, 'unconfined_u:object_r:ssh_home_t:s0') ext_utils.change_owner(pub_path, user_name) ext_utils.add_extension_event(name=hutil.get_name(), op="scenario", is_success=True, message="create-user") hutil.log("Succeeded in resetting ssh_key.") else: err_msg = "Failed to reset ssh key because the cert content is empty." ext_utils.add_extension_event(name=hutil.get_name(), op=constants.WALAEventOperation.Enable, is_success=False, message="(02100)" + err_msg) else: # do the certificate conversion # we support PKCS8 certificates besides ssh-rsa public keys _save_cert_str_as_file(cert_txt, 'temp.crt') pub_path = ovf_env.prepare_dir(pub_path, MyDistro) retcode = ext_utils.run_command_and_write_stdout_to_file( [constants.Openssl, 'x509', '-in', 'temp.crt', '-noout', '-pubkey'], "temp.pub") if retcode > 0: raise Exception("Failed to generate public key file.") MyDistro.ssh_deploy_public_key('temp.pub', pub_path) os.remove('temp.pub') os.remove('temp.crt') ext_utils.add_extension_event(name=hutil.get_name(), op="scenario", is_success=True, message="create-user") hutil.log("Succeeded in resetting ssh_key.") except Exception as e: hutil.log(str(e)) ext_utils.add_extension_event(name=hutil.get_name(), op=constants.WALAEventOperation.Enable, is_success=False, message="(02100)Failed to reset ssh key.") raise e def _get_other_sudoers(user_name): sudoers_file = '/etc/sudoers.d/waagent' if not os.path.isfile(sudoers_file): return None sudoers = ext_utils.get_file_contents(sudoers_file).split("\n") pattern = '^{0}\s'.format(user_name) sudoers = list(filter(lambda x: re.match(pattern, x) is None, sudoers)) return sudoers def _save_other_sudoers(sudoers): sudoers_file = '/etc/sudoers.d/waagent' if sudoers is None: return ext_utils.append_file_contents(sudoers_file, "\n".join(sudoers)) os.chmod("/etc/sudoers.d/waagent", 0o440) def _allow_password_auth(hutil): name = "PasswordAuthentication" _backup_and_update_sshd_config(hutil, name, "yes") cloudInitConfigPath = "/etc/ssh/sshd_config.d/50-cloud-init.conf" config = ext_utils.get_file_contents(cloudInitConfigPath) if config is not None: config = config.split("\n") _set_sshd_config(config, name, "yes") ext_utils.replace_file_with_contents_atomic(cloudInitConfigPath, "\n".join(config)) def _backup_and_update_sshd_config(hutil, attr_name, attr_value): config = ext_utils.get_file_contents(SshdConfigPath).split("\n") for i in range(0, len(config)): if config[i].startswith(attr_name) and attr_value in config[i].lower(): hutil.log("%s already set to %s in sshd_config, skip update." % (attr_name, attr_value)) return hutil.log("Setting %s to %s in sshd_config." % (attr_name, attr_value)) _backup_sshd_config(hutil) _set_sshd_config(config, attr_name, attr_value) ext_utils.replace_file_with_contents_atomic(SshdConfigPath, "\n".join(config)) def _set_sshd_config(config, name, val): notfound = True i = None for i in range(0, len(config)): if config[i].startswith(name): config[i] = "{0} {1}".format(name, val) notfound = False elif config[i].startswith("Match"): # Match block must be put in the end of sshd config break if notfound: if i is None: i = 0 config.insert(i, "{0} {1}".format(name, val)) return config def _get_default_ssh_config_filename(): if OSName is not None: # the default ssh config files are present in # /var/lib/waagent/Microsoft.OSTCExtensions.VMAccessForLinux-<version>/resources/ if re.search("centos", OSName, re.IGNORECASE): return "centos_default" if re.search("debian", OSName, re.IGNORECASE): return "debian_default" if re.search("fedora", OSName, re.IGNORECASE): return "fedora_default" if re.search("red\s?hat", OSName, re.IGNORECASE): return "redhat_default" if re.search("suse", OSName, re.IGNORECASE): return "SuSE_default" if re.search("ubuntu", OSName, re.IGNORECASE): return "ubuntu_default" return "default" def _reset_sshd_config(hutil, restore_backup_ssh): ssh_default_config_filename = _get_default_ssh_config_filename() ssh_default_config_file_path = os.path.join(os.getcwd(), 'resources', ssh_default_config_filename) if not os.path.exists(ssh_default_config_file_path): ssh_default_config_file_path = os.path.join(os.getcwd(), 'resources', 'default') if restore_backup_ssh: if os.path.exists(SshdConfigBackupPath): ssh_default_config_file_path = SshdConfigBackupPath # handle CoreOS differently if isinstance(MyDistro, dist_utils.CoreOSDistro): # Parse sshd port from ssh_default_config_file_path sshd_port = 22 regex = re.compile(r"^Port\s+(\d+)", re.VERBOSE) with open(ssh_default_config_file_path) as f: for line in f: match = regex.match(line) if match: sshd_port = match.group(1) break # Prepare cloud init config for coreos-cloudinit f = tempfile.NamedTemporaryFile(delete=False) f.close() cfg_tempfile = f.name cfg_content = "#cloud-config\n\n" # Overwrite /etc/ssh/sshd_config cfg_content += "write_files:\n" cfg_content += " - path: {0}\n".format(SshdConfigPath) cfg_content += " permissions: 0600\n" cfg_content += " owner: root:root\n" cfg_content += " content: |\n" for line in ext_utils.get_file_contents(ssh_default_config_file_path).split('\n'): cfg_content += " {0}\n".format(line) # Change the sshd port in /etc/systemd/system/sshd.socket cfg_content += "\ncoreos:\n" cfg_content += " units:\n" cfg_content += " - name: sshd.socket\n" cfg_content += " command: restart\n" cfg_content += " content: |\n" cfg_content += " [Socket]\n" cfg_content += " ListenStream={0}\n".format(sshd_port) cfg_content += " Accept=yes\n" ext_utils.set_file_contents(cfg_tempfile, cfg_content) ext_utils.run(['coreos-cloudinit', '-from-file', cfg_tempfile], chk_err=False) os.remove(cfg_tempfile) else: shutil.copyfile(ssh_default_config_file_path, SshdConfigPath) if ssh_default_config_file_path == SshdConfigBackupPath: hutil.log("sshd_config restored from backup, remove backup file.") # Remove backup config once sshd_config restored os.remove(ssh_default_config_file_path) MyDistro.restart_ssh_service() def _backup_sshd_config(hutil): if os.path.exists(SshdConfigPath) and not os.path.exists(SshdConfigBackupPath): # Create VMAccess cache folder if doesn't exist if not os.path.exists(os.path.dirname(SshdConfigBackupPath)): os.makedirs(os.path.dirname(SshdConfigBackupPath)) hutil.log("Create backup ssh config file") open(SshdConfigBackupPath, 'a').close() # When copying, make sure to preserve permissions and ownership. ownership = os.stat(SshdConfigPath) shutil.copy2(SshdConfigPath, SshdConfigBackupPath) os.chown(SshdConfigBackupPath, ownership.st_uid, ownership.st_gid) def _save_cert_str_as_file(cert_txt, file_name): cert_start = cert_txt.find(BeginCertificateTag) if cert_start >= 0: cert_txt = cert_txt[cert_start + len(BeginCertificateTag):] cert_end = cert_txt.find(EndCertificateTag) if cert_end >= 0: cert_txt = cert_txt[:cert_end] cert_txt = cert_txt.strip() cert_txt = "{0}\n{1}\n{2}\n".format(BeginCertificateTag, cert_txt, EndCertificateTag) ext_utils.set_file_contents(file_name, cert_txt) def _open_ssh_port(): _del_rule_if_exists(['INPUT', '-p', 'tcp', '-m', 'tcp', '--dport', '22', '-j', 'DROP']) _del_rule_if_exists(['INPUT', '-p', 'tcp', '-m', 'tcp', '--dport', '22', '-j', 'REJECT']) _del_rule_if_exists(['INPUT', '-p', '-j', 'DROP']) _del_rule_if_exists(['INPUT', '-p', '-j', 'REJECT']) _insert_rule_if_not_exists(['INPUT', '-p', 'tcp', '-m', 'tcp', '--dport', '22', '-j', 'ACCEPT']) _del_rule_if_exists(['OUTPUT', '-p', 'tcp', '-m', 'tcp', '--sport', '22', '-j', 'DROP']) _del_rule_if_exists(['OUTPUT', '-p', 'tcp', '-m', 'tcp', '--sport', '22', '-j', 'REJECT']) _del_rule_if_exists(['OUTPUT', '-p', '-j', 'DROP']) _del_rule_if_exists(['OUTPUT', '-p', '-j', 'REJECT']) _insert_rule_if_not_exists(['OUTPUT', '-p', 'tcp', '-m', 'tcp', '--dport', '22', '-j', 'ACCEPT']) def _del_rule_if_exists(rule_string): rule_string_for_cmp = " ".join(rule_string) cmd_result = ext_utils.run_command_get_output(['iptables-save']) while cmd_result[0] == 0 and (rule_string_for_cmp in cmd_result[1]): ext_utils.run(['iptables', '-D'] + rule_string) cmd_result = ext_utils.run_command_get_output(['iptables-save']) def _insert_rule_if_not_exists(rule_string): rule_string_for_cmp = " ".join(rule_string) cmd_result = ext_utils.run_command_get_output(['iptables-save']) if cmd_result[0] == 0 and (rule_string_for_cmp not in cmd_result[1]): ext_utils.run_command_get_output(['iptables', '-I'] + rule_string) def check_and_repair_disk(hutil): public_settings = hutil.get_public_settings() if public_settings: check_disk = public_settings.get('check_disk') repair_disk = public_settings.get('repair_disk') disk_name = public_settings.get('disk_name') if check_disk and repair_disk: err_msg = ("check_disk and repair_disk was both specified." "Only one of them can be specified") hutil.error(err_msg) hutil.do_exit(1, 'Enable', 'error', '0', 'Enable failed.') if check_disk: ext_utils.add_extension_event(name=hutil.get_name(), op="scenario", is_success=True, message="check_disk") outretcode = _fsck_check(hutil) hutil.log("Successfully checked disk") return outretcode if repair_disk: ext_utils.add_extension_event(name=hutil.get_name(), op="scenario", is_success=True, message="repair_disk") outdata = _fsck_repair(hutil, disk_name) hutil.log("Repaired and remounted disk") return outdata def _fsck_check(hutil): try: retcode = ext_utils.run(['fsck', '-As', '-y']) if retcode > 0: hutil.log(retcode) raise Exception("Disk check was not successful") else: return retcode except Exception as e: hutil.error("Failed to run disk check with error: {0}, {1}".format( str(e), traceback.format_exc())) hutil.do_exit(1, 'Check', 'error', '0', 'Check failed.') def _fsck_repair(hutil, disk_name): # first unmount disks and loop devices lazy + forced try: cmd_result = ext_utils.run(['umount', '-f', '/' + disk_name]) if cmd_result != 0: # Fail fast hutil.log("Failed to unmount disk: %s" % disk_name) # run repair retcode = ext_utils.run(['fsck', '-AR', '-y']) hutil.log("Ran fsck with return code: %d" % retcode) if retcode == 0: retcode, output = ext_utils.run_command_get_output(["mount"]) hutil.log(output) return output else: raise Exception("Failed to mount disks") except Exception as e: hutil.error("{0}, {1}".format(str(e), traceback.format_exc())) hutil.do_exit(1, 'Repair', 'error', '0', 'Repair failed.') if __name__ == '__main__': main()