1""" 2 SoftLayer.CLI.environment 3 ~~~~~~~~~~~~~~~~~~~~~~~~~ 4 Abstracts everything related to the user's environment when running the CLI 5 6 :license: MIT, see LICENSE for more details. 7""" 8import importlib 9 10import click 11import pkg_resources 12 13import SoftLayer 14from SoftLayer.CLI import formatting 15from SoftLayer.CLI import routes 16 17# pylint: disable=too-many-instance-attributes, invalid-name, no-self-use 18 19# Calling pkg_resources.iter_entry_points shows a false-positive 20# pylint: disable=no-member 21 22 23class Environment(object): 24 """Provides access to the current CLI environment.""" 25 26 def __init__(self): 27 # {'path:to:command': ModuleLoader()} 28 # {'vs:list': ModuleLoader()} 29 self.commands = {} 30 self.aliases = {} 31 32 self.vars = {} 33 34 self.client = None 35 self.format = 'table' 36 self.skip_confirmations = False 37 self.config_file = None 38 39 self._modules_loaded = False 40 41 def out(self, output, newline=True): 42 """Outputs a string to the console (stdout).""" 43 click.echo(output, nl=newline) 44 45 def err(self, output, newline=True): 46 """Outputs an error string to the console (stderr).""" 47 click.echo(output, nl=newline, err=True) 48 49 def fmt(self, output, fmt=None): 50 """Format output based on current the environment format.""" 51 if fmt is None: 52 fmt = self.format 53 return formatting.format_output(output, fmt) 54 55 def format_output_is_json(self): 56 """Return True if format output is json or jsonraw""" 57 return 'json' in self.format 58 59 def fout(self, output, newline=True): 60 """Format the input and output to the console (stdout).""" 61 if output is not None: 62 try: 63 self.out(self.fmt(output), newline=newline) 64 except UnicodeEncodeError: 65 # If we hit an undecodeable entry, just try outputting as json. 66 self.out(self.fmt(output, 'json'), newline=newline) 67 68 def input(self, prompt, default=None, show_default=True): 69 """Provide a command prompt.""" 70 return click.prompt(prompt, default=default, show_default=show_default) 71 72 def getpass(self, prompt, default=None): 73 """Provide a password prompt.""" 74 password = click.prompt(prompt, hide_input=True, default=default) 75 76 # https://github.com/softlayer/softlayer-python/issues/1436 77 # click.prompt uses python's getpass() in the background 78 # https://github.com/python/cpython/blob/3.9/Lib/getpass.py#L97 79 # In windows, shift+insert actually inputs the below 2 characters 80 # If we detect those 2 characters, need to manually read from the clipbaord instead 81 # https://stackoverflow.com/questions/101128/how-do-i-read-text-from-the-clipboard 82 if password == 'àR': 83 # tkinter is a built in python gui, but it has clipboard reading functions. 84 # pylint: disable=import-outside-toplevel 85 from tkinter import Tk 86 tk_manager = Tk() 87 password = tk_manager.clipboard_get() 88 # keep the window from showing 89 tk_manager.withdraw() 90 return password 91 92 # Command loading methods 93 def list_commands(self, *path): 94 """Command listing.""" 95 path_str = ':'.join(path) 96 97 commands = [] 98 for command in self.commands: 99 100 # Filter based on prefix and the segment length 101 if all([command.startswith(path_str), 102 len(path) == command.count(":")]): 103 104 # offset is used to exclude the path that the caller requested. 105 offset = len(path_str) + 1 if path_str else 0 106 commands.append(command[offset:]) 107 108 return sorted(commands) 109 110 def get_command(self, *path): 111 """Return command at the given path or raise error.""" 112 path_str = ':'.join(path) 113 114 if path_str in self.commands: 115 return self.commands[path_str].load() 116 117 return None 118 119 def resolve_alias(self, path_str): 120 """Returns the actual command name. Uses the alias mapping.""" 121 if path_str in self.aliases: 122 return self.aliases[path_str] 123 return path_str 124 125 def load(self): 126 """Loads all modules.""" 127 if self._modules_loaded is True: 128 return 129 130 self.load_modules_from_python(routes.ALL_ROUTES) 131 self.aliases.update(routes.ALL_ALIASES) 132 self._load_modules_from_entry_points('softlayer.cli') 133 134 self._modules_loaded = True 135 136 def load_modules_from_python(self, route_list): 137 """Load modules from the native python source.""" 138 for name, modpath in route_list: 139 if ':' in modpath: 140 path, attr = modpath.split(':', 1) 141 else: 142 path, attr = modpath, None 143 self.commands[name] = ModuleLoader(path, attr=attr) 144 145 def _load_modules_from_entry_points(self, entry_point_group): 146 """Load modules from the entry_points (slower). 147 148 Entry points can be used to add new commands to the CLI. 149 150 Usage: 151 152 entry_points={'softlayer.cli': ['new-cmd = mymodule.new_cmd.cli']} 153 154 """ 155 for obj in pkg_resources.iter_entry_points(group=entry_point_group, 156 name=None): 157 self.commands[obj.name] = obj 158 159 def ensure_client(self, config_file=None, is_demo=False, proxy=None): 160 """Create a new SLAPI client to the environment. 161 162 This will be a no-op if there is already a client in this environment. 163 """ 164 if self.client is not None: 165 return 166 167 # Environment can be passed in explicitly. This is used for testing 168 if is_demo: 169 client = SoftLayer.BaseClient( 170 transport=SoftLayer.FixtureTransport(), 171 auth=None, 172 ) 173 else: 174 # Create SL Client 175 client = SoftLayer.create_client_from_env( 176 proxy=proxy, 177 config_file=config_file, 178 ) 179 self.client = client 180 181 182class ModuleLoader(object): 183 """Module loader that acts a little like an EntryPoint object.""" 184 185 def __init__(self, import_path, attr=None): 186 self.import_path = import_path 187 self.attr = attr 188 189 def load(self): 190 """load and return the module/attribute.""" 191 module = importlib.import_module(self.import_path) 192 if self.attr: 193 return getattr(module, self.attr) 194 return module 195 196 197pass_env = click.make_pass_decorator(Environment, ensure=True) 198