def test_kubernetes_discovery(cfgdir, mocker): """ Testing Kubernetes initializaion. Is fabric creating Stub by default? Does Kubernetes return []? """ k8s = os.path.join( os.path.dirname(__file__), "..", "fixture", "k8s.cloud.yaml", ) k8s_data = os.path.join( os.path.dirname(__file__), "..", "fixture", "k8s_discovery_data.yaml", ) k8s_pods = [] with open(k8s_data, "r") as data: k8s_pods = yaml.safe_load(data) cloud = CloudSelect(cfgdir) profile = cloud.configuration_read() profile = cloud.merge(profile, cloud.configuration_read(k8s)) args = cloud.parse_args([]) cloud.fabric(profile, args) assert Container.discovery().__class__.__name__ == "Kubernetes" mocker.patch.object(Kubernetes, "get_pods", return_value=k8s_pods) representation, instances = Container.discovery().run() assert len(instances) == 2 assert representation == [36, 37, 21, 7, 7, 11]
def test_options_errors(caplog, cfgdir): """Test error messages when options block is incorrect.""" caplog.set_level(logging.INFO) configuration = os.path.join( os.path.dirname(__file__), "..", "fixture", "metadata.json", ) logging.Logger.manager.loggerDict.clear() cloud = CloudSelect(cfgdir) configuration = cloud.configuration_read() args = cloud.parse_args([]) configuration["group"] = {"type": "simple"} cloud.fabric(configuration, args) assert isinstance(Container.group(), Simple) caplog.clear() Container.options("option") assert len(caplog.records) == 1 assert (caplog.records[0].getMessage() == "option: 'options' block not found in {'type': 'simple'}") caplog.clear() configuration["group"]["options"] = 123 Container.options("option") assert len(caplog.records) == 1 assert (caplog.records[0].msg == "%s: 'options' block should be list of dictionaries in %s")
def get_option(selected): """Get option block for report.""" # get first instance # assume that all instances match the same group/pattern instance = next(iter(selected), None) if instance: return Container.options("option", instance.metadata) return Container.options("option")
def options(self, name, metadata=None): """Get plugin/block options.""" group = Container.group() base = copy.deepcopy(Container.config().get(name, {})) override = group.run(name, metadata) if override is None: return base if override == {}: return {} return self.merge(base, override)
def test_bastion_initialization(cfgdir): """Assert plugin initialization.""" cloud = CloudSelect(cfgdir) configuration = cloud.configuration_read() args = cloud.parse_args([]) configuration["pathfinder"] = {"type": "bastion"} cloud.fabric(configuration, args) assert isinstance(Container.pathfinder(), Bastion) result = Container.pathfinder().run(INSTANCE, [INSTANCE]) assert result.jumphost is None
def profile_process(self): """Run selection process for the specific profile.""" discovery = Container.discovery() pathfinder = Container.pathfinder() profile_name = Container.args().profile report = Container.report() self.logger.debug("Process profile '%s'", profile_name) representation_maximum_field_length, instances = discovery.run() if not instances: sys.exit("Error: No instances found") selected = self.fzf_select(representation_maximum_field_length, instances) selected = [pathfinder.run(i, instances) for i in selected] return report.run(selected)
def test_stub_pathfinder(cfgdir): """ Testing Stub initializaion. Is fabric creating Stub by default? Does Stub return {}? Is Stub singleton? """ cloud = CloudSelect(cfgdir) # Read shared part profile = cloud.configuration_read() args = cloud.parse_args([]) cloud.fabric(profile, args) assert Container.pathfinder().__class__.__name__ == "Stub" assert Container.pathfinder().run(INSTANCE, [INSTANCE]) == INSTANCE assert Container.pathfinder() == Container.pathfinder()
def test_stub_discovery(cfgdir): """ Testing Stub initializaion. Is fabric creating Stub by default? Does Stub return []? Is Stub singleton? """ cloud = CloudSelect(cfgdir) # Read shared part profile = cloud.configuration_read() args = cloud.parse_args([]) cloud.fabric(profile, args) assert Container.discovery().__class__.__name__ == "Stub" assert Container.discovery().run() == [] assert Container.discovery() == Container.discovery()
def instances(self): """Collect Hetzner instances.""" # Array with maximum field length for each element in representation fields_length = [] for i in self.find(): instance_id = i["id"] metadata = i config = Container.options("discovery", metadata) ip = self.get_ip(i) key = self.get_key(config) port = self.get_port(config) user = self.get_user(config) representation = [instance_id, ip] self.enrich_representation(representation, metadata) # Update maximum field length for idx, value in enumerate(representation): if idx >= len(fields_length): fields_length.append(len(value)) else: fields_length[idx] = max(fields_length[idx], len(value)) instance = CloudInstance( instance_id, ip, None, metadata, representation, key, user, port, ) yield instance yield fields_length
def test_stub_group(tmpdir): """ Testing Stub initializaion. Is fabric creating Stub by default? Does Stub return {}? Is Stub singleton? """ cloud = CloudSelect(tmpdir) # Read shared part profile = cloud.configuration_read() args = cloud.parse_args([]) cloud.fabric(profile, args) assert Container.group().__class__.__name__ == "Stub" assert Container.group().run("aws", METADATA) is None assert Container.group() == Container.group()
def run(self, instance, instances): """Enrich instance with jumphost if any.""" if not instance: raise ValueError("instance must be something, not None") arguments = Container.options("pathfinder", instance.metadata) if arguments: jumphost = None if "host" in arguments: jumphost = CloudInstance( -1, arguments["host"], None, {}, [], arguments.get("key"), arguments.get("user"), arguments.get("port", 22), ) elif "metadata" in arguments and ":" in arguments["metadata"]: key, pattern = arguments["metadata"].split(":", 1) for i in instances: point = self.find_jumphost(key, pattern, i) if point: if point == instance: break # We don't like to add bastion for itself jumphost = point break if jumphost: return attr.evolve(instance, jumphost=jumphost) return instance
def test_bastion_behaviour(cfgdir): """Assert bastion returning correct result.""" cloud = CloudSelect(cfgdir) configuration = cloud.configuration_read() args = cloud.parse_args([]) configuration["pathfinder"] = { "type": "bastion", "host": "my-bastion-hostname" } cloud.fabric(configuration, args) assert isinstance(Container.pathfinder(), Bastion) result = Container.pathfinder().run(INSTANCE, [INSTANCE]) assert isinstance(result.jumphost, CloudInstance) assert result.jumphost.host == "my-bastion-hostname"
def complete(self, cline, cpoint): """List profiles for shell completion.""" configpath = Container.configpath() extensions = Container.extensions() prefix = cline[0:cpoint].partition(" ")[-1] self.logger.debug( "Complete line %s, point %s, prefix %s", cline, cpoint, prefix, ) profiles = [] for profile in os.listdir(configpath): for extension in extensions: if profile.endswith(".{}".format(extension)): name = profile.replace(".{}".format(extension), "") if name.startswith(prefix): profiles.append(name) print("\n".join(sorted(set(profiles))))
def profile_list(self): """List available profiles.""" configpath = Container.configpath() extensions = Container.extensions() self.logger.debug("List all available profiles from %s", configpath) profiles = [] print("CloudSelect profiles:") for profile in os.listdir(configpath): for extension in extensions: if profile.endswith(".{}".format(extension)): profiles.append( "- {}".format(profile.replace(".{}".format(extension), "")), ) if profiles: print("\n".join(sorted(set(profiles)))) else: print("- NO PROFILES -")
def reporter_list(): """List available reporters.""" empty = True print("CloudSelect reporters:") for reporter in sorted( Container.config().get("plugin", {}).get("report", {}).keys(), ): empty = False print("- {}".format(reporter)) if empty: print("- NO REPORTERS -")
def fabric(self, configuration, args): """Load discovery, group, pathfinder, report plugins.""" if args.verbose: if args.verbose == 1: configuration.get("log", {}).get("root", {})["level"] = logging.INFO elif args.verbose > 1: configuration.get("log", {}).get("root", {})["level"] = logging.DEBUG dictConfig(configuration.get("log", {})) self.log = logging.getLogger("cloudselect.CloudSelect") self.log.debug("Logging is initialized") self.log.debug( "Configuration:\n%s", json.dumps(configuration, sort_keys=True, indent=4, separators=(",", ": ")), ) Container.args = providers.Object(args) Container.config = providers.Configuration(name="config", default=configuration) Container.configpath = providers.Object(self.configpath) Container.extensions = providers.Object(self.extensions) Container.options = providers.Callable(self.options) Container.selector = providers.Singleton(Selector) Container.discovery = self.fabric_load_plugin( configuration, "discovery", DiscoveryServiceProvider, DiscoveryStub, ) Container.group = self.fabric_load_plugin( configuration, "group", GroupServiceProvider, GroupStub, ) Container.pathfinder = self.fabric_load_plugin( configuration, "pathfinder", PathFinderServiceProvider, PathFinderStub, ) Container.report = self.fabric_load_plugin( configuration, "report", ReportServiceProvider, ReportStub, args.reporter, ) return Container.selector()
def select(self): """Entry point. Select instances.""" args = Container.args() configpath = Container.configpath() extensions = Container.extensions() if args.edit is None or args.edit: if args.edit is None: configuration = os.path.join(configpath, "{}".format(extensions[0])) return self.edit(configuration) for extension in extensions: profile = os.path.join(configpath, "{}.{}".format(args.edit, extension)) if os.path.isfile(profile): return self.edit(profile) print("Profile '{}' does not exist".format(args.edit), file=sys.stderr) return self.logger.error("Profile '%s' does not exist", args.edit) if args.reporter is None: return self.reporter_list() if not args.profile: return self.profile_list() return self.profile_process()
def test_select_single(cfgdir): """Testing that Selector automaticaly select the only one available instance.""" profile = os.path.join(os.path.dirname(__file__), "fixture", "single.cloud.json") cloud = CloudSelect(cfgdir) args = cloud.parse_args([profile]) configuration = cloud.configuration_read() configuration = cloud.merge(configuration, cloud.configuration_read(args.profile)) assert args.profile == profile selector = cloud.fabric(configuration, args) assert isinstance(Container.discovery(), Local) report = selector.select() assert len(report["instances"]) == 1 assert report["instances"][0]["host"] == "my.cloud.instance"
def test_options_regex(cfgdir): """Test regex against metadata mock.""" metadata = os.path.join(os.path.dirname(__file__), "..", "fixture", "metadata.json") cloud = CloudSelect(cfgdir) configuration = cloud.configuration_read() args = cloud.parse_args([]) configuration["group"] = { "type": "simple", "options": [{ "match": "Tags.Name:my-app.abc", "option": {} }], } configuration["option"] = {"ssh": "-t"} cloud.fabric(configuration, args) assert isinstance(Container.group(), Simple) with open(metadata) as json_file: data = json.load(json_file) assert Container.options("option") == {"ssh": "-t"} assert Container.options("option", data) == {}
def test_select_empty(cfgdir): """Testing that Selector exits if there is no any instances.""" profile = os.path.join(os.path.dirname(__file__), "fixture", "empty.cloud.json") cloud = CloudSelect(cfgdir) args = cloud.parse_args([profile]) configuration = cloud.configuration_read() configuration = cloud.merge(configuration, cloud.configuration_read(args.profile)) assert args.profile == profile selector = cloud.fabric(configuration, args) assert isinstance(Container.discovery(), Local) with pytest.raises(SystemExit) as pytest_wrapped_e: selector.select() assert pytest_wrapped_e.type == SystemExit assert pytest_wrapped_e.value.code == "Error: No instances found"
def get_ip(self, instance, config): """Get instance IP.""" profile_name = Container.args().profile region = instance["Placement"]["AvailabilityZone"][:-1] ip = self.get_option(config.get("ip", {}), None, profile_name, region) if ip == "public": return instance.get("PublicIpAddress", "") if ip == "private": return instance.get("PrivateIpAddress", "") if ip == "public_private": return instance.get("PublicIpAddress", instance.get("PrivateIpAddress", "")) if ip == "private_public": return instance.get("PrivateIpAddress", instance.get("PublicIpAddress", "")) return instance.get("PublicIpAddress", instance.get("PrivateIpAddress", ""))
def config(): """Return selector configuration.""" return Container.config().get("option", {})
def config(): """Return discovery configuration.""" return Container.config().get("discovery", {})
def config(): """Return group configuration.""" return Container.config().get("group", {})
def test_options(cfgdir): """ Test options behaviour of simple plugin. It should returns {} if there is no options. It should returns shared dictionary if there is no any matched filters. It should returns clarified dictionary if there is matched filter. """ cloud = CloudSelect(cfgdir) configuration = cloud.configuration_read() args = cloud.parse_args([]) configuration["group"] = {"type": "simple"} cloud.fabric(configuration, args) assert isinstance(Container.group(), Simple) assert Container.options("test") == {} assert Container.options("plugin") == configuration["plugin"] assert Container.options("log") == configuration["log"] assert Container.options("option") == {} options_a = {"ssh": "-t", "ssh_command": "sudo -i"} options_b = {"ssh": "-t", "ssh_command": "su"} options_c = {"ssh": "-t", "ssh_command": None} configuration["option"] = options_a assert Container.options("option") == options_a configuration["group"]["options"] = [{"match": "x:y", "option": options_a}] assert Container.options("option") == options_a assert Container.options("option", {"a": {"b": "c123"}}) == options_a configuration["group"]["options"].append({ "match": "a.b:c123", "option": options_b }) assert Container.options("option") == options_a assert Container.options("option", {"a": {"b": "c123"}}) == options_b configuration["group"]["options"].append( { "match": "a.b:c(.*3|111)", "option": options_c }, ) assert Container.options("option") == options_a assert Container.options("option", {"a": {"b": "c1nnnn3"}}) == options_c assert Container.options("option", {"a": {"b": "c111111"}}) == options_c assert Container.options("option", {"a": {"b": "c112111"}}) == options_a
def get_key(self, host): """Get key for ssh host.""" self.logger.debug("Search SSH key for %s", host) config = Container.options("discovery", host) return (config.get("key") or config.get("key", {}).get(host) or config.get("key", {}).get("*"))
def get_user(self, host): """Get user for SSH host.""" self.logger.debug("Search user for %s", host) config = Container.options("discovery", host) return (config.get("user") or config.get("user", {}).get(host) or config.get("user", {}).get("*"))