def get_access_token(self): # Validate service account services = self.k8s_client.execute_native_cmd("kubectl get serviceaccount -o json") all_service_account = json.loads(services) if not self.get_specific_service(all_service_account, "kubeapps-operator"): self.k8s_client.execute_native_cmd(cmd="kubectl create serviceaccount kubeapps-operator") # Validate clusterrolebinding services = self.k8s_client.execute_native_cmd("kubectl get clusterrolebinding -o json") all_cluster_rolebinding = json.loads(services) if not self.get_specific_service(all_cluster_rolebinding, "kubeapps-operator"): self.k8s_client.execute_native_cmd( cmd="kubectl create clusterrolebinding kubeapps-operator --clusterrole=cluster-admin --serviceaccount=default:kubeapps-operator" ) try: cmd = r"""kubectl get secret $(kubectl get serviceaccount kubeapps-operator -o jsonpath='{range .secrets[*]}{.name}{"\n"}{end}' | grep kubeapps-operator-token) -o jsonpath='{.data.token}' -o go-template='{{.data.token | base64decode}}'""" self.access_token = self.k8s_client.execute_native_cmd(cmd=cmd) except Exception as ex: raise StopChatFlow( "There is an issue happened during getting access token to be able to access kubeapps solution" ) if not self.access_token: raise StopChatFlow( "There is an issue happened during getting access token to be able to access kubeapps solution" )
def create_pool(self): owner = self.threebot_name threebot = get_threebot_config_instance(owner, self.threebot_info["solution_uuid"]) zos = get_threebot_zos(threebot) identity = generate_user_identity(threebot, self.password, zos) zos = j.sals.zos.get(identity.instance_name) farm_name, existent_pool = self._find_free_pool_in_farm(zos, self.available_farms) if existent_pool is not None: self.pool_id = existent_pool.pool_id else: farm = random.choice(self.available_farms) farm_name = farm.name self.pool_info = deployer.create_3bot_pool( farm_name, self.expiration, currency=self.currency, identity_name=identity.instance_name, **self.query, ) if self.pool_info.escrow_information.address.strip() == "": raise StopChatFlow( f"provisioning the pool, invalid escrow information probably caused by a misconfigured, pool creation request was {self.pool_info}" ) payment_info = deployer.pay_for_pool(self.pool_info) result = deployer.wait_pool_reservation(self.pool_info.reservation_id, bot=self) if not result: raise StopChatFlow(f"provisioning the pool timed out. pool_id: {self.pool_info.reservation_id}") self.md_show_update( f"Capacity pool {self.pool_info.reservation_id} created and funded with {payment_info['total_amount_dec']} TFT" ) self.pool_id = self.pool_info.reservation_id
def _get_custom_domain(self): self.md_show_update("Preparing gateways ...") gateways = deployer.list_all_gateways(self.username, self.farm_name) if not gateways: raise StopChatFlow( "There are no available gateways in the farms bound to your pools. The resources you paid for will be re-used in your upcoming deployments." ) gateway_values = list(gateways.values()) random.shuffle(gateway_values) self.addresses = [] for gw_dict in gateway_values: gateway = gw_dict["gateway"] if not gateway.dns_nameserver: continue self.addresses = [] for ns in gateway.dns_nameserver: try: ip_address = j.sals.nettools.get_host_by_name(ns) except Exception as e: j.logger.error( f"failed to resolve nameserver {ns} of gateway {gateway.node_id} due to error {str(e)}" ) continue self.addresses.append(ip_address) if self.addresses: self.gateway = gateway self.gateway_pool = gw_dict["pool"] self.domain = self.string_ask( "Please specify the domain name you wish to bind to", required=True) self.domain = j.sals.zos.get().gateway.correct_domain( self.domain) res = """\ ## Waiting for DNS Population... Please create an `A` record in your DNS manager for domain: `{{domain}}` pointing to: {% for ip in addresses -%} - {{ ip }} {% endfor %} """ res = j.tools.jinja2.render_template(template_text=res, addresses=self.addresses, domain=self.domain) self.md_show_update(dedent(res), md=True) # wait for domain name to be created if not self.wait_domain(self.domain, self.addresses): raise StopChatFlow( "The specified domain name is not pointing to the gateway properly! Please bind it and try again. The resource you paid for will be re-used for your next deployment." ) return self.domain raise StopChatFlow( "No available gateways. The resource you paid for will be re-used for your next deployment" )
def ask_multi_pool_placement( self, username, bot, number_of_nodes, resource_query_list=None, pool_ids=None, workload_names=None ): """ Ask and schedule workloads accross multiple pools Args: bot: chatflow object number_of_nodes: number of required nodes for deployment resource_query_list: list of query dicts {"cru": 1, "sru": 2, "mru": 1, "hru": 1}. if specified it must be same length as number_of_nodes pool_ids: if specfied it will limit the pools shown in the chatflow to only these pools workload_names: if specified they will shown when asking the user for node selection for each workload. if specified it must be same length as number_of_nodes Returns: ([], []): first list contains the selected node objects. second list contains selected pool ids """ resource_query_list = resource_query_list or [dict()] * number_of_nodes workload_names = workload_names or [None] * number_of_nodes if len(resource_query_list) != number_of_nodes: raise StopChatFlow("resource query_list must be same length as number of nodes") if len(workload_names) != number_of_nodes: raise StopChatFlow("workload_names must be same length as number of nodes") pools = self.list_pools(username) if pool_ids: filtered_pools = {} for pool_id in pools: if pool_id in pool_ids: filtered_pools[pool_id] = pools[pool_id] pools = filtered_pools selected_nodes = [] selected_pool_ids = [] for i in range(number_of_nodes): cloud_units = self.calculate_capacity_units(**resource_query_list[i]) cu, su = cloud_units.cu, cloud_units.su pool_choices = {} for p in pools: if pools[p][0] < cu or pools[p][1] < su: continue nodes = j.sals.zos.get().nodes_finder.nodes_by_capacity(pool_id=p, **resource_query_list[i]) if not nodes: continue pool_choices[p] = pools[p] pool_id = self.select_pool( username, bot, available_pools=pool_choices, workload_name=workload_names[i], cu=cu, su=su ) node = self.ask_container_placement(bot, pool_id, workload_name=workload_names[i], **resource_query_list[i]) if not node: node = self.schedule_container(pool_id, **resource_query_list[i]) selected_nodes.append(node) selected_pool_ids.append(pool_id) return selected_nodes, selected_pool_ids
def ip_config(self): ips = ["IPv6", "IPv4"] self.ipversion = self.single_choice( "How would you like to connect to your network? If unsure, choose IPv4", ips, required=True, default="IPv4", ) self.md_show_update("Searching for access node...") pools = [ p for p in j.sals.zos.pools.list() if p.node_ids and p.cus >= 0 and p.sus >= 0 and p.empty_at > j.data.time.now().timestamp ] self.access_node = None for pool in pools: try: access_nodes = j.sals.reservation_chatflow.reservation_chatflow.get_nodes( 1, ip_version=self.ipversion, pool_ids=[pool.pool_id] ) except StopChatFlow: continue if access_nodes: self.access_node = access_nodes[0] self.pool = pool.pool_id break if not self.access_node: raise StopChatFlow("There are no available access nodes in your existing pools") if self.action == "Create": self.ip_range = j.sals.reservation_chatflow.reservation_chatflow.get_ip_range(self)
def network_reservation(self): if self.action == "Create": try: self.config = deployer.deploy_network( self.solution_name, self.access_node, self.ip_range, self.ipversion, self.pool, **self.solution_metadata, ) except Exception as e: raise StopChatFlow(f"Failed to register workload due to error {str(e)}") else: self.config = deployer.add_access( self.network_view.name, self.network_view, self.access_node.node_id, self.pool, self.ipversion == "IPv4", bot=self, **self.solution_metadata, ) for wid in self.config["ids"]: try: success = deployer.wait_workload(wid, self, breaking_node_id=self.access_node.node_id) except StopChatFlow as e: if self.action == "Create": solutions.cancel_solution(self.config["ids"]) raise e if not success: raise DeploymentFailed(f"Failed to deploy workload {wid}", wid=wid)
def select_domain(self): gateways = deployer.list_all_gateways() if not gateways: raise StopChatFlow( "There are no available gateways in the farms bound to your pools." ) domains = dict() for gw_dict in gateways.values(): gateway = gw_dict["gateway"] for domain in gateway.managed_domains: domains[domain] = gw_dict self.domain = self.single_choice( "Please choose the domain you wish to use", list(domains.keys()), required=True) self.gateway = domains[self.domain]["gateway"] self.gateway_pool = domains[self.domain]["pool"] self.domain = f"{self.threebot_name}-{self.solution_name}.{self.domain}" self.domain = j.sals.zos.get().gateway.correct_domain(self.domain) self.addresses = [] for ns in self.gateway.dns_nameserver: self.addresses.append(j.sals.nettools.get_host_by_name(ns)) self.secret = f"{j.core.identity.me.tid}:{uuid.uuid4().hex}"
def reservation(self): self.resv_id = deployer.delegate_domain( self.pool_id, self.gateway_id, self.domain, **self.solution_metadata, solution_uuid=self.solution_id ) success = deployer.wait_workload(self.resv_id, self) if not success: raise StopChatFlow(f"Failed to deploy workload {self.resv_id}")
def _get_kube_config(self): if j.sals.vdc.list_all(): self.vdc_name = list(j.sals.vdc.list_all())[0] else: raise StopChatFlow(f"No Virtual Data Centers(VDC) were found.", htmlAlert=True) self.vdc_info = {} self.vdc = j.sals.vdc.find(name=self.vdc_name, load_info=True) self.identity_name = j.core.identity.me.instance_name self.secret = f"{self.vdc.identity_tid}:{uuid.uuid4().hex}" for node in self.vdc.kubernetes: if node.role == KubernetesRole.MASTER: self.vdc_info["master_ip"] = node.ip_address self.vdc_info["pool_id"] = node.pool_id self.vdc_info["public_ip"] = node.public_ip self.vdc_info[ "farm_name"] = j.core.identity.me.explorer.farms.get( j.core.identity.me.explorer.nodes.get( node.node_id).farm_id).name self.vdc_info["kube_config_path"] = j.sals.fs.expanduser( "~/.kube/config") self.vdc_info["network_name"] = self.vdc_name self.vdc_info["network_view"] = deployer.get_network_view( self.vdc_name, identity_name=self.identity_name) break
def _create_identities(self): instance_name = self.solution_name threebot_name = self.threebot_name tname = f"{threebot_name}_{instance_name}" email = f"{tname}@threefold.me" words = j.data.encryption.key_to_mnemonic( self.backup_password.encode().zfill(32)) self.mainnet_identity_name = f"{tname}_main" self.testnet_identity_name = f"{tname}_test" try: if "testnet" in j.core.identity.me.explorer_url: self.identity_name = self.testnet_identity_name identity_test = j.core.identity.get( self.testnet_identity_name, tname=tname, email=email, words=words, explorer_url="https://explorer.testnet.grid.tf/api/v1", ) self._register_identity(threebot_name, identity_test) else: self.identity_name = self.mainnet_identity_name identity_main = j.core.identity.get( self.mainnet_identity_name, tname=tname, email=email, words=words, explorer_url="https://explorer.grid.tf/api/v1", ) self._register_identity(threebot_name, identity_main) except: raise StopChatFlow( f"Couldn't register new identity with the given name {tname}. Make sure you entered the correct password." )
def pool_start(self): self.md_show_update("It will take a few seconds to be ready to help you ...") # check stellar service if not j.clients.stellar.check_stellar_service(): raise StopChatFlow("Payment service is currently down, try again later") self.pool_id = self.kwargs["pool_id"]
def _get_available_farms(self, only_one=True, identity_name=None): if getattr(self, "available_farms", None) is not None: return self.currency = getattr(self, "currency", "TFT") farm_message = f"""\ Fetching available farms.. """ self.md_show_update(dedent(farm_message)) self.available_farms = [] farms = j.sals.zos.get(identity_name)._explorer.farms.list() # farm_names = ["freefarm"] # DEUBGGING ONLY for farm in farms: farm_name = farm.name available_ipv4, _, _, _, _ = deployer.check_farm_capacity( farm_name, currencies=[self.currency], ip_version="IPv4", **self.query) if available_ipv4: self.available_farms.append(farm) if only_one: return if not self.available_farms: raise StopChatFlow( "No available farms with enough resources for this deployment at the moment" )
def select_network(self, username, bot): network_views = self.list_networks(username) network_names = [n[len(username) + 1 :] for n in network_views.keys()] if not network_views: raise StopChatFlow(f"You don't have any deployed network.") network_name = bot.single_choice("Please select a network", network_names, required=True) return network_views[f"{username}_{network_name}"]
def start(self): super().start() if not self.kwargs.get("name"): self.network_view = deployer.select_network(self) else: self.network_view = deployer.get_network_view(self.kwargs["name"]) if not self.network_view: raise StopChatFlow(f"no network named {self.kwargs['name']}")
def init_new_user(self, bot, username, farm_name, expiration, currency, **resources): pool_info = self.create_solution_pool(bot, username, farm_name, expiration, currency, **resources) qr_code = self.show_payment(pool_info, bot) result = self.wait_pool_reservation(pool_info.reservation_id, qr_code=qr_code, bot=bot) if not result: raise StopChatFlow(f"Waiting for pool payment timedout. pool_id: {pool_info.reservation_id}") wgcfg = self.init_new_user_network(bot, username, pool_info.reservation_id) return pool_info, wgcfg
def _init(self): self.md_show_update( "It will take a few seconds to be ready to help you ...") # check stellar service if not j.clients.stellar.check_stellar_service(): raise StopChatFlow( "Payment service is currently down, try again later") # xlms check for wname in ["activation_wallet"]: if wname in j.clients.stellar.list_all(): try: w = j.clients.stellar.get(wname) if w.get_balance_by_asset("XLM") < 10: raise StopChatFlow( f"{wname} doesn't have enough XLM to support the deployment." ) except: raise StopChatFlow( f"Couldn't get the balance for {wname} wallet") else: j.logger.info(f"{wname} is funded") else: j.logger.info(f"This system doesn't have {wname} configured") # tft wallets check for wname in [ self.VDC_INIT_WALLET_NAME, self.GRACE_PERIOD_WALLET_NAME ]: try: w = j.clients.stellar.get(wname) if w.get_balance_by_asset() < 50: raise StopChatFlow( f"{wname} doesn't have enough TFT to support the deployment." ) except: raise StopChatFlow( f"Couldn't get the balance for {wname} wallet") else: j.logger.info(f"{wname} is funded") self.user_info_data = self.user_info() self.username = self.user_info_data["username"]
def _validate_resource_limits(self, cpu, memory, no_nodes=1): queries = [{"cpu": cpu, "memory": memory}] * no_nodes if self.ADDITIONAL_QUERIES: queries += self.ADDITIONAL_QUERIES monitor = self.vdc.get_kubernetes_monitor() if not monitor.check_deployment_resources(queries): wids = monitor.extend(bot=self) if not wids: raise StopChatFlow( f"There are not enough resources to deploy cpu: {cpu}, memory: {memory}." )
def different_farm(self): self.md_show_update("Checking payment service...") # check stellar service if not j.clients.stellar.check_stellar_service(): raise StopChatFlow("Payment service is currently down, try again later") self.diff_farm = False diff_farm = self.single_choice( "Do you want to deploy this node on a different farm?", options=["Yes", "No"], default="No", required=True ) if diff_farm == "Yes": self.diff_farm = True
def create_pool(self): owner = self.threebot_name threebot = get_threebot_config_instance(owner, self.threebot_info["solution_uuid"]) zos = get_threebot_zos(threebot) identity = generate_user_identity(threebot, self.password, zos) zos = j.sals.zos.get(identity.instance_name) farm = random.choice(self.available_farms) farm_name = farm.name self.pool_info = deployer.create_3bot_pool( farm_name, self.expiration, currency=self.currency, identity_name=identity.instance_name, **self.query, ) if self.pool_info.escrow_information.address.strip() == "": raise StopChatFlow( f"provisioning the pool, invalid escrow information probably caused by a misconfigured, pool creation request was {self.pool_info}" ) msg, qr_code = deployer.get_qr_code_payment_info(self.pool_info) deployer.msg_payment_info = msg result = deployer.wait_pool_payment(self, self.pool_info.reservation_id, qr_code=qr_code) if not result: raise StopChatFlow(f"provisioning the pool timed out. pool_id: {self.pool_info.reservation_id}") self.pool_id = self.pool_info.reservation_id
def _init_solution(self): self.md_show_update("Checking payment service...") # check stellar service if not j.clients.stellar.check_stellar_service(): raise StopChatFlow( "Payment service is currently down, try again later") self._validate_user() self.solution_id = uuid.uuid4().hex self.solution_metadata = {} self.username = self.user_info()["username"] self.solution_metadata["owner"] = self.username self.threebot_name = j.data.text.removesuffix(self.username, ".3bot") self.expiration = 60 * 60 * 3 # expiration 3 hours
def access_node_selection(self): self.md_show_update("Fetching Access Nodes...") pools = [ p for p in j.sals.zos.get().pools.list() if p.node_ids and p.cus >= 0 and p.sus >= 0 and p.empty_at > j.data.time.now().timestamp ] access_nodes_pools = defaultdict(list) for p in pools: for node_id in p.node_ids: access_nodes_pools[node_id].append(p.pool_id) available_access_nodes = {} all_access_nodes = filter( lambda node: node.node_id in access_nodes_pools, j.sals.zos.get()._explorer.nodes.list()) if self.ipversion == "IPv4": ip_filter = j.sals.zos.get().nodes_finder.filter_public_ip4 else: ip_filter = j.sals.zos.get().nodes_finder.filter_public_ip6 available_access_nodes = { n.node_id: n for n in all_access_nodes if ip_filter(n) and j.sals.zos.get().nodes_finder.filter_is_up(n) } if not available_access_nodes: raise StopChatFlow( "There are no available access nodes in your existing pools") access_node_id = self.drop_down_choice( "Please select an access node or leave it empty to automatically select it", list(available_access_nodes.keys()), ) if access_node_id: self.access_node = available_access_nodes[access_node_id] if len(access_nodes_pools[self.access_node.node_id]) > 1: self.pool = self.drop_down_choice( "Please select a pool or leave it empty to automaically select it", access_nodes_pools[self.access_node.node_id], ) if not self.pool: self.pool = random.choice( list(access_nodes_pools[self.access_node.node_id])) else: self.pool = access_nodes_pools[self.access_node.node_id][0] else: self.access_node = random.choice( list(available_access_nodes.values())) self.pool = random.choice( list(access_nodes_pools[self.access_node.node_id]))
def _get_pool(self): self._get_available_farms(only_one=False, identity_name=self.identity_name) self._select_farms() self._select_pool_node() self.pool_info = deployer.create_3bot_pool( self.farm_name, self.expiration, currency=self.currency, identity_name=self.identity_name, **self.query, ) if self.pool_info.escrow_information.address.strip() == "": raise StopChatFlow( f"provisioning the pool, invalid escrow information probably caused by a misconfigured, pool creation request was {self.pool_info}" ) payment_info = deployer.pay_for_pool(self.pool_info) result = deployer.wait_pool_reservation(self.pool_info.reservation_id, bot=self) if not result: raise StopChatFlow( f"provisioning the pool timed out. pool_id: {self.pool_info.reservation_id}" ) self.md_show_update( f"Capacity pool {self.pool_info.reservation_id} created and funded with {payment_info['total_amount_dec']} TFT" ) gevent.sleep(2) self.wgcfg = deployer.init_new_user_network( self, self.identity_name, self.pool_info.reservation_id, identity_name=self.identity_name, network_name="management", ) self.md_show_update("Management network created.") self.pool_id = self.pool_info.reservation_id self.network_view = deployer.get_network_view( "management", identity_name=self.identity_name)
def solution_extension(self): self.currencies = ["TFT"] self.pool_info, self.qr_code = deployer.extend_solution_pool( self, self.pool_id, self.expiration, self.currencies, **self.query) if self.pool_info and self.qr_code: # cru = 1 so cus will be = 0 result = deployer.wait_pool_payment(self, self.pool_id, qr_code=self.qr_code, trigger_sus=self.pool.sus + 1) if not result: raise StopChatFlow( f"Waiting for pool payment timedout. pool_id: {self.pool_id}" )
def pool_start(self): self.md_show_update("Checking payment service...") # check stellar service if not j.clients.stellar.check_stellar_service(): raise StopChatFlow("Payment service is currently down, try again later") self.pools = [p for p in j.sals.zos.get().pools.list() if p.node_ids] if not self.pools: self.action = "create" else: self.action = self.single_choice( "Would you like to create a new capacity pool, or extend an existing one?", ["create", "extend"], required=True, default="create", )
def _validate_user(self): tname = self.user_info()["username"].lower() user_factory = StoredFactory(UserEntry) explorer_url = j.core.identity.me.explorer.url if "testnet" in explorer_url: explorer_name = "testnet" elif "devnet" in explorer_url: explorer_name = "devnet" elif "explorer.grid.tf" in explorer_url: explorer_name = "mainnet" else: raise StopChatFlow(f"Unsupported explorer {explorer_url}") instance_name = f"{explorer_name}_{tname.replace('.3bot', '')}" if instance_name in user_factory.list_all(): user_entry = user_factory.get(instance_name) if not user_entry.has_agreed: raise StopChatFlow( f"You must accept terms and conditions before using this solution. please head towards the main page to read our terms" ) else: raise StopChatFlow( f"You must accept terms and conditions before using this solution. please head towards the main page to read our terms" )
def network_reservation(self): try: self.config = deployer.add_access( self.network_view.name, self.network_view, self.access_node.node_id, self.pool, self.ipversion == "IPv4", bot=self, **self.solution_metadata, ) except Exception as e: raise StopChatFlow(f"Failed to register workload due to error {str(e)}") super().network_reservation()
def network_reservation(self): try: self.config = deployer.deploy_network( self.solution_name, self.access_node, self.ip_range, self.ipversion, self.pool, **self.solution_metadata, ) except Exception as e: raise StopChatFlow( f"Failed to register workload due to error {str(e)}") super().network_reservation()
def pay_for_pool(self, pool): info = self.get_payment_info(pool) WALLET_NAME = j.sals.marketplace.deployer.WALLET_NAME wallet = j.clients.stellar.get(name=WALLET_NAME) # try payment for 5 mins zos = j.sals.zos.get() now = j.data.time.utcnow().timestamp while j.data.time.utcnow().timestamp <= now + 5 * 60: try: zos.billing.payout_farmers(wallet, pool) return info except InsufficientFunds as e: raise e except Exception as e: j.logger.warning(str(e)) raise StopChatFlow(f"Failed to pay for pool {pool} in time, Please try again later")
def _forward_ports(self, port_forwards=None): """ portforwards = {"<SERVICE_NAME>": {"src":<src_port>, "dest": <dest_port>} } example: portforwards = { "mysql": {"src": 7070, "dest": 3306, "protocol": "TCP"}, "redis": {"src": 6060, "dest", 6379, "protocol": "TCP"} } """ ssh_client = self.get_k8s_sshclient() port_forwards = port_forwards or {} for service, ports in port_forwards.items(): if ports.get("src") and ports.get("dest"): # Validate if the port not exposed if self.is_port_exposed(ssh_client, ports.get("src")): j.logger.critical( f"VDC: Can not expose service with port {ports.get('src')} using socat, port already in use" ) raise StopChatFlow( f"VDC: Can not expose service with port {ports.get('src')} using socat, port already in use" ) cluster_ip = self.k8s_client.execute_native_cmd( f"kubectl get service/{service} -o jsonpath='{{.spec.clusterIP}}'" ) if cluster_ip and j.sals.fs.exists("/root/.ssh/id_rsa"): socat = "/var/lib/rancher/k3s/data/current/bin/socat" cmd = f"{socat} tcp-listen:{ports['src']},reuseaddr,fork tcp:{cluster_ip}:{ports['dest']}" template = f"""#!/sbin/openrc-run name="{service}" command="{cmd}" pidfile="/var/run/{service}.pid" command_background=true """ template = dedent(template) file_name = f"{self.config.release_name}-socat-{service}" rc, out, err = ssh_client.sshclient.run( f"sudo touch /etc/init.d/{file_name} && sudo chmod 777 /etc/init.d/{file_name} && echo '{template}' >> /etc/init.d/{file_name} && sudo rc-service {file_name} start", warn=True, ) if rc: j.logger.critical( f"VDC: Can not expose service using socat error was rc:{rc}, out:{out}, error:{err}" ) return True return False
def _ask_for_node(self): nodes = deployer.get_all_farms_nodes(self.available_farms, **self.query) if not nodes: raise StopChatFlow( f"no nodes available to deploy 3bot with resources: {self.query}" ) node_id_dict = {node.node_id: node for node in nodes} node_id = self.drop_down_choice( "Please select the node you would like to deploy your 3Bot on.", list(node_id_dict.keys()), required=True, ) self.selected_node = node_id_dict[node_id] self.available_farms = [ farm for farm in self.available_farms if farm.id == self.selected_node.farm_id ] self.retries = 1