Skip to content

Commit

Permalink
New hostgroup parameter
Browse files Browse the repository at this point in the history
  • Loading branch information
charles-001 committed Feb 9, 2024
1 parent c00c554 commit 2433ead
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 95 deletions.
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,21 +77,22 @@ options:
--config-file Dolphie's config file to use [default: ~/.dolphie]
--mycnf-file MySQL config file path to use. This should use [client] section. See below for options support [default: ~/.my.cnf]
-f , --host-cache-file
Resolve IPs to hostnames when your DNS is unable to. Each IP/hostname pair should be on its own line using format: ip=hostname [default: ~/dolphie_host_cache]
Resolve IPs to hostnames when your DNS is unable to. Each IP/hostname pair should be on its own line using format ip=hostname [default: ~/dolphie_host_cache]
-q , --host-setup-file
Specify location of file that stores the available hosts to use in host setup modal [default: ~/dolphie_hosts]
-l , --login-path Specify login path to use mysql_config_editor's file ~/.mylogin.cnf for encrypted login credentials. Supercedes config file [default: client]
-l , --login-path Specify login path to use with mysql_config_editor's file ~/.mylogin.cnf for encrypted login credentials. Supercedes config file [default: client]
-r , --refresh_interval
How much time to wait in seconds between each refresh [default: 1]
-H , --heartbeat-table
If your hosts use pt-heartbeat, specify table in format db.table to use the timestamp it has for replication lag instead of Seconds_Behind_Master from SHOW SLAVE STATUS
If your hosts use pt-heartbeat, specify table in format db.table to use the timestamp it has for replication lag instead of Seconds_Behind_Master from SHOW REPLICA STATUS
--ssl-mode Desired security state of the connection to the host. Supports: REQUIRED/VERIFY_CA/VERIFY_IDENTITY [default: OFF]
--ssl-ca Path to the file that contains a PEM-formatted CA certificate
--ssl-cert Path to the file that contains a PEM-formatted client certificate
--ssl-key Path to the file that contains a PEM-formatted private key for the client certificate
--panels What panels to display on startup separated by a comma. Supports: dashboard/processlist/graphs/replication/locks/ddl [default: dashboard,processlist]
--graph-marker What marker to use for graphs (available options: https://tinyurl.com/dolphie-markers) [default: braille]
--pypi-repository What PyPi repository to use when checking for a new version. If not specified, it will use Dolphie's PyPi repository
--hostgroup This is used for creating tabs and connecting to them for hosts you specify in Dolphie's config file under a hostgroup section. As an example, you'll have a section called [cluster1] then below it will be listed each host on a new line in the format key=host where key can be anything you want
--show-trxs-only Start with only showing threads that have an active transaction
--additional-columns Start with additional columns in Processlist panel
--historical-locks Always run the locks query so it can save historical data to its graph instead of only when the Locks panel is open. This query can be expensive in some environments
Expand Down
49 changes: 44 additions & 5 deletions dolphie/Modules/ArgumentParser.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,21 @@ class Config:
show_trxs_only: bool = False
show_additional_query_columns: bool = False
historical_locks: bool = False
hostgroup_hosts: List[str] = field(default_factory=list)


class ArgumentParser:
def __init__(self, app_version: str):
self.config_options = {}
for variable in fields(Config):
if variable.name not in ["app_version", "config_file", "host_setup_available_hosts", "ssl"]:
# Exclude these options since they are manually configured
if variable.name not in [
"app_version",
"config_file",
"host_setup_available_hosts",
"ssl",
"hostgroup_hosts",
]:
self.config_options[variable.name] = variable.type

self.formatted_options = "\n\t".join(
Expand Down Expand Up @@ -156,7 +164,7 @@ def _add_options(self):
type=str,
help=(
"Resolve IPs to hostnames when your DNS is unable to. Each IP/hostname pair should be on its own line "
f"using format: ip=hostname [default: {self.config.host_cache_file}]"
f"using format ip=hostname [default: {self.config.host_cache_file}]"
),
metavar="",
)
Expand All @@ -177,8 +185,8 @@ def _add_options(self):
dest="login_path",
type=str,
help=(
"Specify login path to use mysql_config_editor's file ~/.mylogin.cnf for encrypted login credentials. "
f"Supercedes config file [default: {self.config.login_path}]"
"Specify login path to use with mysql_config_editor's file ~/.mylogin.cnf for encrypted login"
f" credentials. Supercedes config file [default: {self.config.login_path}]"
),
metavar="",
)
Expand All @@ -197,7 +205,7 @@ def _add_options(self):
type=str,
help=(
"If your hosts use pt-heartbeat, specify table in format db.table to use the timestamp it "
"has for replication lag instead of Seconds_Behind_Master from SHOW SLAVE STATUS"
"has for replication lag instead of Seconds_Behind_Master from SHOW REPLICA STATUS"
),
metavar="",
)
Expand Down Expand Up @@ -262,6 +270,18 @@ def _add_options(self):
),
metavar="",
)
self.parser.add_argument(
"--hostgroup",
dest="hostgroup",
type=str,
help=(
"This is used for creating tabs and connecting to them for hosts you specify in"
" Dolphie's config file under a hostgroup section. As an example, you'll have a section"
" called [cluster1] then below it will be listed each host on a new line in the format"
" key=host where key can be anything you want"
),
metavar="",
)
self.parser.add_argument(
"--show-trxs-only",
dest="show_trxs_only",
Expand Down Expand Up @@ -421,6 +441,25 @@ def _parse(self):
self.config.pypi_repository = options["pypi_repository"]
self.config.historical_locks = options["historical_locks"]

hostgroup = options["hostgroup"]
if hostgroup:
if os.path.isfile(self.config.config_file):
cfg = ConfigParser()
cfg.read(self.config.config_file)

if cfg.has_section(hostgroup):
for key in cfg.options(hostgroup):
value = cfg.get(hostgroup, key).strip()
if value:
self.config.hostgroup_hosts.append(value)
else:
sys.exit(self.console.print(f"Hostgroup '{hostgroup}' has an empty host for key '{key}'"))

else:
sys.exit(self.console.print(f"Hostgroup '{hostgroup}' was not found in Dolphie's config file"))
else:
sys.exit(self.console.print(f"Dolphie's config file was not found at {self.config.config_file}"))

self.config.startup_panels = options["startup_panels"].split(",")
for panel in self.config.startup_panels:
if panel not in self.panels.all():
Expand Down
4 changes: 3 additions & 1 deletion dolphie/Modules/MySQL.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ def __init__(
port: int,
ssl: str,
save_connection_id: bool = True,
auto_connect: bool = True,
):
self.connection: pymysql.Connection = None
self.connection_id: int = None
Expand All @@ -35,7 +36,8 @@ def __init__(
self.max_reconnect_attempts = 3
self.running_query = False

self.connect()
if auto_connect:
self.connect()

def connect(self):
try:
Expand Down
71 changes: 35 additions & 36 deletions dolphie/Modules/TabManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import dolphie.Modules.MetricManager as MetricManager
from dolphie import Dolphie
from dolphie.Modules.ArgumentParser import Config
from dolphie.Modules.ManualException import ManualException
from dolphie.Widgets.host_setup import HostSetupModal
from dolphie.Widgets.spinner import SpinnerWidget
from dolphie.Widgets.topbar import TopBar
Expand Down Expand Up @@ -35,10 +36,11 @@ class Tab:
name: str
dolphie: Dolphie
manual_tab_name: bool = False
connecting_as_hostgroup: bool = False

worker: Worker = None
worker_timer: Timer = None
worker_cancel_error: str = None
worker_cancel_error: ManualException = None
worker_running: bool = False

replicas_worker: Worker = None
Expand Down Expand Up @@ -119,8 +121,9 @@ async def disconnect(self, update_topbar: bool = True):
for member in self.dolphie.app.query(f".replica_container_{self.id}"):
await member.remove()

self.dolphie.read_only_status = "DISCONNECTED"
if update_topbar:
self.update_topbar(f"[[white]DISCONNECTED[/white]] {self.dolphie.host}:{self.dolphie.port}")
self.update_topbar()

def update_topbar(self, custom_text: str = None):
dolphie = self.dolphie
Expand All @@ -129,19 +132,10 @@ def update_topbar(self, custom_text: str = None):
self.topbar.host = custom_text
return

read_only_status = dolphie.read_only_status
if dolphie.main_db_connection and not dolphie.main_db_connection.is_connected():
read_only_status = "DISCONNECTED"
if dolphie.read_only_status:
self.topbar.host = f"[[white]{dolphie.read_only_status}[/white]] {dolphie.mysql_host}"
else:
if not self.worker:
if not self.loading_indicator.display:
self.topbar.host = ""

# If there is no worker instance, we don't update the topbar
return

if read_only_status and dolphie.mysql_host:
self.topbar.host = f"[[white]{read_only_status}[/white]] {dolphie.mysql_host}"
self.topbar.host = ""

def host_setup(self):
dolphie = self.dolphie
Expand Down Expand Up @@ -202,13 +196,17 @@ def __init__(self, app: App, config: Config):
self.tab_id_counter: int = 1

self.tabbed_content = self.app.query_one("#tabbed_content", TabbedContent)
self.tabbed_content.display = False

async def create_tab(self, tab_name: str):
async def create_tab(self, tab_name: str, use_hostgroup: bool = False) -> Tab:
tab_id = self.tab_id_counter

if len(self.app.screen_stack) > 1:
return

await self.tabbed_content.add_pane(
TabPane(
tab_name,
"",
LoadingIndicator(id=f"loading_indicator_{tab_id}"),
SpinnerWidget(id=f"spinner_{tab_id}"),
VerticalScroll(
Expand Down Expand Up @@ -345,8 +343,20 @@ async def create_tab(self, tab_name: str):
Switch(animate=False, id=metric, name=metric_tab_name)
)

# If we're using hostgroups, we want to split the tab name into the host and port
if use_hostgroup and self.config.hostgroup_hosts:
tab_host_split = tab_name.split(":")

if len(tab_host_split) == 1:
self.config.host = tab_host_split[0]
self.config.port = self.config.port
else:
self.config.host = tab_host_split[0]
self.config.port = tab_host_split[1]

# Create a new tab instance
dolphie = Dolphie(config=self.config, app=self.app)

dolphie.tab_id = tab_id
dolphie.tab_name = tab_name

Expand Down Expand Up @@ -433,6 +443,9 @@ async def create_tab(self, tab_name: str):
# Increment the tab id counter
self.tab_id_counter += 1

self.tabbed_content.display = True
return tab

async def remove_tab(self, tab_id: int):
await self.tabbed_content.remove_pane(f"tab_{self.get_tab(tab_id).id}")

Expand All @@ -443,11 +456,10 @@ def rename_tab(self, tab_id: int, new_name: str = None):
tab.manual_tab_name = new_name
else:
if not tab.manual_tab_name:
if tab.dolphie.mysql_host:
# mysql_host is the full host:port string, we want to split & truncate it to 24 characters
host = tab.dolphie.mysql_host.split(":")[0][:24]
else:
host = tab.dolphie.host[:24]
# mysql_host is the full host:port string, we want to split & truncate it to 24 characters
host = tab.dolphie.mysql_host.split(":")[0][:24]
if not host:
return

# If the last character isn't a letter or number, remove it
if not host[-1].isalnum():
Expand All @@ -461,6 +473,8 @@ def rename_tab(self, tab_id: int, new_name: str = None):

def switch_tab(self, tab_id: int):
tab = self.get_tab(tab_id)
if not tab:
return

self.app.tab = tab # Update the current tab variable for the app

Expand All @@ -480,18 +494,3 @@ def get_all_tabs(self) -> list:
all_tabs.append(tab.id)

return all_tabs

def layout_graphs(self, tab: Tab):
if tab.dolphie.is_mysql_version_at_least("8.0.30"):
self.app.query_one(f"#graph_redo_log_{tab.id}").styles.width = "55%"
self.app.query_one(f"#graph_redo_log_bar_{tab.id}").styles.width = "12%"
self.app.query_one(f"#graph_redo_log_active_count_{tab.id}").styles.width = "33%"
tab.dolphie.metric_manager.metrics.redo_log_active_count.Active_redo_log_count.visible = True
self.app.query_one(f"#graph_redo_log_active_count_{tab.id}").display = True
else:
self.app.query_one(f"#graph_redo_log_{tab.id}").styles.width = "88%"
self.app.query_one(f"#graph_redo_log_bar_{tab.id}").styles.width = "12%"
self.app.query_one(f"#graph_redo_log_active_count_{tab.id}").display = False

self.app.query_one(f"#graph_adaptive_hash_index_{tab.id}").styles.width = "50%"
self.app.query_one(f"#graph_adaptive_hash_index_hit_ratio_{tab.id}").styles.width = "50%"
13 changes: 11 additions & 2 deletions dolphie/Widgets/host_setup.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from dolphie.Modules.ManualException import ManualException
from textual import on
from textual.app import ComposeResult
from textual.binding import Binding
Expand Down Expand Up @@ -66,7 +67,15 @@ class HostSetupModal(ModalScreen):
Binding("escape", "app.pop_screen", "", show=False),
]

def __init__(self, host, port, username, password, available_hosts, error_message=None):
def __init__(
self,
host: str,
port: str,
username: str,
password: str,
available_hosts: list,
error_message: ManualException = None,
):
super().__init__()

self.host = host
Expand All @@ -88,7 +97,7 @@ def on_mount(self) -> None:

footer.display = False
if self.error_message:
footer.update(self.error_message)
footer.update(self.error_message.output())
footer.display = True

def compose(self) -> ComposeResult:
Expand Down
29 changes: 15 additions & 14 deletions dolphie/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ def reset_runtime_variables(self):
self.replica_lag_source: str = None
self.replica_lag: int = None
self.active_redo_logs: int = None
self.mysql_host: str = None
self.mysql_host: str = f"{self.host}:{self.port}"
self.binlog_transaction_compression_percentage: int = None
self.host_cache: dict = {}

Expand All @@ -117,20 +117,7 @@ def reset_runtime_variables(self):
self.group_replication_members: dict = {}
self.group_replication_data: dict = {}

# Database connection global_variables
# Main connection is used for Textual's worker thread so it can run asynchronous
self.main_db_connection: Database = None
# Secondary connection is for ad-hoc commands that are not a part of the worker thread
self.secondary_db_connection: Database = None
self.performance_schema_enabled: bool = False
self.use_performance_schema: bool = True
self.server_uuid: str = None
self.mysql_version: str = None
self.host_distro: str = None

self.host_cache_from_file = load_host_cache_file(self.host_cache_file)

def db_connect(self):
db_connection_args = {
"app": self.app,
"tab_name": self.tab_name,
Expand All @@ -140,10 +127,24 @@ def db_connect(self):
"socket": self.socket,
"port": self.port,
"ssl": self.ssl,
"auto_connect": False,
}
self.main_db_connection = Database(**db_connection_args)
# Secondary connection is for ad-hoc commands that are not a part of the worker thread
self.secondary_db_connection = Database(**db_connection_args, save_connection_id=False)

self.performance_schema_enabled: bool = False
self.use_performance_schema: bool = True
self.server_uuid: str = None
self.mysql_version: str = None
self.host_distro: str = None

self.host_cache_from_file = load_host_cache_file(self.host_cache_file)

def db_connect(self):
self.main_db_connection.connect()
self.secondary_db_connection.connect()

global_variables = self.main_db_connection.fetch_status_and_variables("variables")

basedir = global_variables.get("basedir")
Expand Down
Loading

0 comments on commit 2433ead

Please sign in to comment.