From 49acd80a532ee115464978a4c286293d21216cd4 Mon Sep 17 00:00:00 2001 From: Lucas Hoffmann Date: Sat, 24 Jul 2021 12:25:02 +0200 Subject: [PATCH 1/6] hetznerctl: Add config parser for _cmd options Fixes #43. --- hetznerctl | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/hetznerctl b/hetznerctl index ff29c9d..3e6d4f4 100755 --- a/hetznerctl +++ b/hetznerctl @@ -3,6 +3,7 @@ import sys import locale import warnings import argparse +import subprocess from os.path import expanduser @@ -16,6 +17,37 @@ except ImportError: from configparser import RawConfigParser +class SecureConfigParser(RawConfigParser): + """A config parser extension to read *_cmd options trasperantly. + + If an option is queried with get() or has_option() but it does not exist + this class tries to find a corresponing *_cmd option instead and execute + it in a shell. The stdout of the shell command is returned as the config + value. + """ + + @staticmethod + def _run_cmd(cmd): + result = subprocess.run(cmd, capture_output=True) + if result.returncode == 0: + return result.stdout + raise Exception("Command {} exited with error {}: {}".format(cmd, + result.returncode, result.stderr)) + + def has_option(self, section, name): + has = lambda name: super().has_option(section, name) + if name.endswith("_cmd"): + return has(name) + return has(name) or has(name+"_cmd") + + def get(self, section, name): + has = lambda name: super().has_option(section, name) + get = lambda name: super().get(section, name) + if has(name+"_cmd") and not has(name): + return self._run_cmd(get(name+"_cmd")) + return get(name) + + def make_option(*args, **kwargs): return (args, kwargs) @@ -28,7 +60,7 @@ class SubCommand(object): requires_robot = True def __init__(self, configfile): - self.config = RawConfigParser() + self.config = SecureConfigParser() self.config.read(configfile) def putline(self, line): From 601d4b889270144e1d13344e8f46fc71349e0d99 Mon Sep 17 00:00:00 2001 From: Lucas Hoffmann Date: Sun, 25 Jul 2021 21:05:24 +0200 Subject: [PATCH 2/6] hetznerctl: Fix names, typos, etc Noted by the first review and by pycodestyle. --- hetznerctl | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/hetznerctl b/hetznerctl index 3e6d4f4..569d14e 100755 --- a/hetznerctl +++ b/hetznerctl @@ -17,8 +17,8 @@ except ImportError: from configparser import RawConfigParser -class SecureConfigParser(RawConfigParser): - """A config parser extension to read *_cmd options trasperantly. +class CommandConfigParser(RawConfigParser): + """A config parser extension to read *_cmd options transparently. If an option is queried with get() or has_option() but it does not exist this class tries to find a corresponing *_cmd option instead and execute @@ -31,20 +31,20 @@ class SecureConfigParser(RawConfigParser): result = subprocess.run(cmd, capture_output=True) if result.returncode == 0: return result.stdout - raise Exception("Command {} exited with error {}: {}".format(cmd, - result.returncode, result.stderr)) + raise Exception("Command {} exited with error {}: {}".format( + cmd, result.returncode, result.stderr)) def has_option(self, section, name): - has = lambda name: super().has_option(section, name) + def has(name): super().has_option(section, name) if name.endswith("_cmd"): return has(name) - return has(name) or has(name+"_cmd") + return has(name) or has(name + "_cmd") def get(self, section, name): - has = lambda name: super().has_option(section, name) - get = lambda name: super().get(section, name) - if has(name+"_cmd") and not has(name): - return self._run_cmd(get(name+"_cmd")) + def has(name): super().has_option(section, name) + def get(name): super().get(section, name) + if has(name + "_cmd") and not has(name): + return self._run_cmd(get(name + "_cmd")) return get(name) @@ -60,7 +60,7 @@ class SubCommand(object): requires_robot = True def __init__(self, configfile): - self.config = SecureConfigParser() + self.config = CommandConfigParser() self.config.read(configfile) def putline(self, line): From 0f8c13166493dea094dccc0816b8b613336db6d4 Mon Sep 17 00:00:00 2001 From: Lucas Hoffmann Date: Sun, 25 Jul 2021 21:23:11 +0200 Subject: [PATCH 3/6] htznerctl: Do not override methods This also updates the usage of the subprocess module to support python 2.7. --- hetznerctl | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/hetznerctl b/hetznerctl index 569d14e..1843533 100755 --- a/hetznerctl +++ b/hetznerctl @@ -26,25 +26,17 @@ class CommandConfigParser(RawConfigParser): value. """ - @staticmethod - def _run_cmd(cmd): - result = subprocess.run(cmd, capture_output=True) - if result.returncode == 0: - return result.stdout - raise Exception("Command {} exited with error {}: {}".format( - cmd, result.returncode, result.stderr)) - - def has_option(self, section, name): + def has_or_cmd(self, section, name): def has(name): super().has_option(section, name) if name.endswith("_cmd"): return has(name) return has(name) or has(name + "_cmd") - def get(self, section, name): + def get_get_or_cmd(self, section, name): def has(name): super().has_option(section, name) def get(name): super().get(section, name) if has(name + "_cmd") and not has(name): - return self._run_cmd(get(name + "_cmd")) + return subprocess.check_output(get(name + "_cmd"), shell=True) return get(name) @@ -429,8 +421,8 @@ def main(): subcommand = args.cmdclass(args.configfile) if subcommand.requires_robot: - if not subcommand.config.has_option('login', 'username') or \ - not subcommand.config.has_option('login', 'password'): + if not subcommand.config.has_or_cmd('login', 'username') or \ + not subcommand.config.has_or_cmd('login', 'password'): parser.error(( "You need to set a user and password in {0} in order to" " continue with this operation. You can do this using" @@ -438,8 +430,8 @@ def main(): " `hetznerctl config login.password '." ).format(args.configfile)) robot = Robot( - subcommand.config.get('login', 'username'), - subcommand.config.get('login', 'password'), + subcommand.config.get_or_cmd('login', 'username'), + subcommand.config.get_or_cmd('login', 'password'), ) else: robot = None From d80a823a20fce823bd35cc26e23ef63fbab9989b Mon Sep 17 00:00:00 2001 From: Lucas Hoffmann Date: Sun, 25 Jul 2021 21:23:35 +0200 Subject: [PATCH 4/6] hetznerctl: Add new *_cmd options to help ouput --- hetznerctl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/hetznerctl b/hetznerctl index 1843533..ad8b3a3 100755 --- a/hetznerctl +++ b/hetznerctl @@ -428,6 +428,11 @@ def main(): " continue with this operation. You can do this using" " `hetznerctl config login.username ' and" " `hetznerctl config login.password '." + " If you do not want to store your credentials in the config" + " file in plain text you can give a shell command that should" + " print the option value on stdout. Use `hetznerctl config" + " login.username_cmd ' and `hetznerctl" + " config login.username_cmd ` to save them." ).format(args.configfile)) robot = Robot( subcommand.config.get_or_cmd('login', 'username'), From 9944e4d8b86077a830022e2a687a08153b660288 Mon Sep 17 00:00:00 2001 From: Lucas Hoffmann Date: Sun, 25 Jul 2021 23:38:08 +0200 Subject: [PATCH 5/6] hetznerctl: Add sanity and security checks to config parser --- hetznerctl | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/hetznerctl b/hetznerctl index ad8b3a3..c562b6b 100755 --- a/hetznerctl +++ b/hetznerctl @@ -4,6 +4,7 @@ import locale import warnings import argparse import subprocess +import os from os.path import expanduser @@ -17,6 +18,10 @@ except ImportError: from configparser import RawConfigParser +class ConfigError(Exception): + pass + + class CommandConfigParser(RawConfigParser): """A config parser extension to read *_cmd options transparently. @@ -26,19 +31,37 @@ class CommandConfigParser(RawConfigParser): value. """ + @staticmethod + def _check(section, name): + def has(name): super().has_option(section, name) + if has(name) and has(name + "_cmd"): + raise ConfigError( + "Option {}.{} has a corresponing _cmd option." + " At most one of the two is allowed".format(section, name)) + + def has_or_cmd(self, section, name): + self._check(section, name) def has(name): super().has_option(section, name) if name.endswith("_cmd"): return has(name) return has(name) or has(name + "_cmd") def get_get_or_cmd(self, section, name): + self._check(section, name) def has(name): super().has_option(section, name) def get(name): super().get(section, name) if has(name + "_cmd") and not has(name): return subprocess.check_output(get(name + "_cmd"), shell=True) return get(name) + def read(self, filename): + # check that the config file is not accessable for group and others + if os.stat(filename).st_mode not in (0o100600, 0o100400): + raise ConfigError("The config file must only have read and write" + " permissions for the owner.") + super().read(filename) + def make_option(*args, **kwargs): return (args, kwargs) From acfa6c5a2941d503a5727b1b2f332c6ab7b4a556 Mon Sep 17 00:00:00 2001 From: Lucas Hoffmann Date: Tue, 29 Mar 2022 08:06:58 +0200 Subject: [PATCH 6/6] tmp --- hetznerctl | 1 + 1 file changed, 1 insertion(+) diff --git a/hetznerctl b/hetznerctl index c562b6b..1d3d914 100755 --- a/hetznerctl +++ b/hetznerctl @@ -57,6 +57,7 @@ class CommandConfigParser(RawConfigParser): def read(self, filename): # check that the config file is not accessable for group and others + import stat # TODO if os.stat(filename).st_mode not in (0o100600, 0o100400): raise ConfigError("The config file must only have read and write" " permissions for the owner.")