mirror of
https://github.com/Microsoft/sql-server-samples.git
synced 2025-12-08 14:58:54 +00:00
785 lines
27 KiB
Python
785 lines
27 KiB
Python
#!/usr/bin/python3
|
|
from argparse import ArgumentParser
|
|
from copy import deepcopy
|
|
from enum import Enum, IntEnum
|
|
from datetime import datetime
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from tempfile import mkdtemp
|
|
from threading import Timer
|
|
import time
|
|
|
|
from kubernetes import client as kclient, config as kconfig, watch as kwatch
|
|
import yaml
|
|
|
|
from utils import (AgRole, AgMode, OperatorYaml, SqlSecretsYaml,
|
|
PersistentVolumeYaml, PersistentVolumeClaimYaml,
|
|
SqlServerYaml, AgServiceYaml, FailoverYaml)
|
|
|
|
DEFAULT_K8S_AGENTS_IMAGE = (
|
|
"mcr.microsoft.com/mssql/ha:vNext-CTP2.0-ubuntu")
|
|
|
|
|
|
|
|
TEMPLATES_DIR = "templates"
|
|
SQLSERVER_TEMPLATE = os.path.join(TEMPLATES_DIR, "sqlserver.yaml")
|
|
OPERATOR_TEMPLATE = os.path.join(TEMPLATES_DIR, "operator.yaml")
|
|
AG_SERVICE_TEMPLATE = os.path.join(TEMPLATES_DIR, "ag-service.yaml")
|
|
SQL_SECRETS_TEMPLATE = os.path.join(TEMPLATES_DIR, "sql-secrets.yaml")
|
|
PV_TEMPLATE = os.path.join(TEMPLATES_DIR, "pv.yaml")
|
|
PVC_TEMPLATE = os.path.join(TEMPLATES_DIR, "pvc.yaml")
|
|
FAILOVER_YAML_TEMPLATE = os.path.join(TEMPLATES_DIR, "failover.yaml")
|
|
|
|
SQLSERVER_YAML_FILENAME = "sqlserver.yaml"
|
|
OPERATOR_YAML_FILENAME = "operator.yaml"
|
|
AG_SERVICES_YAML_FILENAME = "ag-services.yaml"
|
|
SQL_SECRETS_YAML_FILENAME = "sql-secrets.yaml"
|
|
PV_YAML_FILENAME = "pv.yaml"
|
|
PVC_YAML_FILENAME = "pvc.yaml"
|
|
FAILOVER_YAML_FILENAME = "failover.yaml"
|
|
|
|
SQLSERVER_NAME_PREFIX = "mssql"
|
|
DEFAULT_NUM_SQLSERVER = 3
|
|
DEFAULT_SQLSERVER_NAMES = [
|
|
"{}{}".format(SQLSERVER_NAME_PREFIX, i + 1)
|
|
for i in range(DEFAULT_NUM_SQLSERVER)
|
|
]
|
|
DEFAULT_AG_NAME = "ag1"
|
|
DEFAULT_NAMESPACE = "default"
|
|
|
|
TERM_RED = "\x1b[31m"
|
|
TERM_GREEN = "\x1b[32m"
|
|
TERM_END = "\x1b[0m"
|
|
|
|
|
|
class LogLevel(IntEnum):
|
|
ALL = 0
|
|
ERROR = 1
|
|
WARNING = 2
|
|
INFO = 3
|
|
DEBUG = 4
|
|
|
|
|
|
log_verbosity = LogLevel.ALL
|
|
|
|
|
|
def log(level, *args):
|
|
color = ""
|
|
color_end = ""
|
|
|
|
if level == LogLevel.ERROR:
|
|
color = TERM_RED
|
|
color_end = TERM_END
|
|
if level <= log_verbosity:
|
|
print(color, "[", level.name, "] ", color_end, sep="", end="")
|
|
print(*args)
|
|
|
|
|
|
# YAML read/write functions
|
|
def create_operator_yaml(namespace,
|
|
k8s_agent_image,
|
|
operator_template=OPERATOR_TEMPLATE,
|
|
filepath=OPERATOR_YAML_FILENAME):
|
|
with open(operator_template) as f:
|
|
operator_yaml = OperatorYaml(yaml.load_all(f.read()))
|
|
operator_yaml.set_namespace(namespace)
|
|
operator_yaml.set_agent_image(k8s_agent_image)
|
|
with open(filepath, "w") as operator_yaml_file:
|
|
yaml.dump_all(operator_yaml.data, operator_yaml_file)
|
|
log(LogLevel.INFO, "operator YAML file:", filepath)
|
|
|
|
|
|
def get_pv_name(namespace, sqlserver_name):
|
|
return "{}-{}-pv".format(namespace, sqlserver_name)
|
|
|
|
|
|
def create_pv_yaml(namespace,
|
|
sqlservers,
|
|
root_path,
|
|
pv_template=PV_TEMPLATE,
|
|
filepath=PV_YAML_FILENAME):
|
|
with open(pv_template) as f:
|
|
pv_yaml_template = PersistentVolumeYaml(yaml.load(f.read()))
|
|
pv_yaml_list = []
|
|
for sqlserver_name in sqlservers:
|
|
pv_yaml = pv_yaml_template.copy()
|
|
pv_yaml.set_storage(namespace)
|
|
pv_name = get_pv_name(namespace, sqlserver_name)
|
|
pv_yaml.set_name(pv_name)
|
|
pv_yaml.set_path(os.path.join(root_path, pv_name))
|
|
pv_yaml_list.append(pv_yaml)
|
|
|
|
with open(filepath, "w") as pv_yaml_file:
|
|
yaml.dump_all([pv.data for pv in pv_yaml_list], pv_yaml_file)
|
|
log(LogLevel.INFO, "Persistent Volume YAML file:", filepath)
|
|
return pv_yaml_list
|
|
|
|
|
|
# def create_pvc_yaml(namespace, name, storage_class_name="default",
|
|
# pvc_template=PVC_TEMPLATE, filepath=PVC_YAML_FILENAME):
|
|
# with open(pv_template) as f:
|
|
# pvc_yaml_template = PersistentVolumeClaimYaml(yaml.load(f.read()))
|
|
|
|
|
|
def create_sqlservers_yaml(env,
|
|
namespace,
|
|
sqlservers,
|
|
ags,
|
|
sa_password,
|
|
k8s_agent_image,
|
|
sqlserver_template=SQLSERVER_TEMPLATE,
|
|
filepath=SQLSERVER_YAML_FILENAME):
|
|
with open(sqlserver_template) as f:
|
|
sqlserver_yaml_template = SqlServerYaml(yaml.load_all(f.read()))
|
|
sqlserver_yaml_list = []
|
|
for sqlserver_name in sqlservers:
|
|
sqlserver_yaml = sqlserver_yaml_template.copy()
|
|
sqlserver_yaml.set_name(sqlserver_name)
|
|
sqlserver_yaml.set_namespace(namespace)
|
|
sqlserver_yaml.set_availability_groups(ags)
|
|
sqlserver_yaml.set_agent_image(k8s_agent_image)
|
|
if env is Environment.ON_PREM:
|
|
sqlserver_yaml.set_volume_claim_template_with_selector(
|
|
namespace)
|
|
sqlserver_yaml.set_service_type("NodePort")
|
|
elif env is Environment.AKS:
|
|
sqlserver_yaml.set_volume_claim_template_with_storage_class()
|
|
sqlserver_yaml.set_service_type("LoadBalancer")
|
|
else:
|
|
raise ValueError("Invalid Environment type")
|
|
for data in sqlserver_yaml.data:
|
|
sqlserver_yaml_list.append(data)
|
|
|
|
with open(filepath, "w") as sqlserver_yaml_file:
|
|
yaml.dump_all(sqlserver_yaml_list, sqlserver_yaml_file)
|
|
log(LogLevel.INFO, "SQL Server YAML file:", filepath)
|
|
|
|
|
|
def create_ag_services_yaml(env,
|
|
namespace,
|
|
ag,
|
|
ag_service_template=AG_SERVICE_TEMPLATE,
|
|
filepath=AG_SERVICES_YAML_FILENAME):
|
|
with open(ag_service_template) as f:
|
|
ag_service_yaml = AgServiceYaml(yaml.load(f.read()))
|
|
if env is Environment.ON_PREM:
|
|
ag_service_yaml_list = create_ag_services(
|
|
"NodePort", ag_service_yaml.data, namespace=namespace, ag=ag,annotations=env.service_annotations)
|
|
elif env is Environment.AKS:
|
|
ag_service_yaml_list = create_ag_services(
|
|
"LoadBalancer",
|
|
ag_service_yaml.data,
|
|
namespace=namespace,
|
|
ag=ag,
|
|
annotations=env.service_annotations)
|
|
else:
|
|
raise ValueError("Invalid env")
|
|
ag_services_yaml_raw = [
|
|
ags_yaml.data for ags_yaml in ag_service_yaml_list
|
|
]
|
|
with open(filepath, "w") as ag_services_yaml_file:
|
|
yaml.dump_all(ag_services_yaml_raw, ag_services_yaml_file)
|
|
log(LogLevel.INFO, "ag-services YAML file:", filepath)
|
|
return ag_service_yaml_list
|
|
|
|
|
|
def create_ag_services(service_type, template_data, namespace, ag,
|
|
annotations):
|
|
primary = AgServiceYaml.create_ag_service(service_type,
|
|
deepcopy(template_data),
|
|
namespace, ag, AgRole.PRIMARY,
|
|
None, annotations)
|
|
sec_sync = AgServiceYaml.create_ag_service(service_type,
|
|
deepcopy(template_data),
|
|
namespace, ag, AgRole.SECONDARY,
|
|
AgMode.SYNC, annotations)
|
|
sec_async = AgServiceYaml.create_ag_service(
|
|
service_type, deepcopy(template_data), namespace, ag, AgRole.SECONDARY,
|
|
AgMode.ASYNC, annotations)
|
|
sec_config_only = AgServiceYaml.create_ag_service(
|
|
service_type, deepcopy(template_data), namespace, ag, AgRole.SECONDARY,
|
|
AgMode.CONFIG, annotations)
|
|
|
|
return (primary, sec_sync, sec_async, sec_config_only)
|
|
|
|
|
|
def create_sql_secrets_yaml(namespace,
|
|
sapassword,
|
|
sql_secrets_template=SQL_SECRETS_TEMPLATE,
|
|
filepath=SQL_SECRETS_YAML_FILENAME):
|
|
with open(sql_secrets_template) as f:
|
|
sql_secrets_yaml = SqlSecretsYaml(yaml.load(f.read()))
|
|
sql_secrets_yaml.set_namespace(namespace)
|
|
sql_secrets_yaml.set_sapassword(sapassword)
|
|
with open(filepath, "w") as sql_secretes_yaml_file:
|
|
yaml.dump_all([sql_secrets_yaml.data], sql_secretes_yaml_file)
|
|
log(LogLevel.INFO, "sql-secrets YAML file:", filepath)
|
|
return sql_secrets_yaml
|
|
|
|
|
|
def create_failover_yaml(namespace,
|
|
agent_image,
|
|
ag,
|
|
target_replica,
|
|
failover_template=FAILOVER_YAML_TEMPLATE,
|
|
filepath=FAILOVER_YAML_FILENAME):
|
|
with open(failover_template) as f:
|
|
failover_yaml = FailoverYaml(yaml.load_all(f.read()))
|
|
failover_yaml.set_namespace(namespace)
|
|
failover_yaml.set_config_map_name(ag)
|
|
failover_yaml.set_endpoint_name(ag)
|
|
failover_yaml.set_failover_container(
|
|
agent_image=agent_image, ag=ag, new_primary=target_replica)
|
|
with open(filepath, "w") as failover_yaml_file:
|
|
yaml.dump_all(failover_yaml.data, failover_yaml_file)
|
|
log(LogLevel.INFO, "failover YAML file:", filepath)
|
|
return failover_yaml
|
|
|
|
|
|
# kubectl functions
|
|
def kubectl(args, **kwargs):
|
|
proc_args = ["kubectl"] + args
|
|
log(LogLevel.ALL, "Running command:", "`{}`".format(" ".join(proc_args)))
|
|
proc = subprocess.run(
|
|
args=proc_args,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
universal_newlines=True,
|
|
**kwargs)
|
|
if proc.stdout:
|
|
log(LogLevel.INFO, proc_args, "\n", proc.stdout)
|
|
if proc.stderr:
|
|
log(LogLevel.ERROR, proc_args, "\n", proc.stderr)
|
|
log(LogLevel.INFO, proc_args, "exit code:", proc.returncode)
|
|
return proc
|
|
|
|
|
|
def deploy_operator(namespace, operator_yaml):
|
|
# We use `kubectl` here because for the kubernetes client we would
|
|
# need to handle the document kind individually.
|
|
kubectl(["apply", "-f", operator_yaml])
|
|
operator_deployed = False
|
|
|
|
def is_operator_deployed(event):
|
|
if (event["type"] == "ADDED"
|
|
and event["object"].metadata.name == "mssql-operator"):
|
|
return True, event["object"]
|
|
else:
|
|
return False, event["object"]
|
|
|
|
apps_api = kclient.AppsV1Api()
|
|
operator_success, operator_deployed = kube_watch_event(
|
|
is_operator_deployed,
|
|
apps_api.list_namespaced_deployment,
|
|
namespace=namespace,
|
|
timeout_seconds=10)
|
|
log(LogLevel.ALL, "operator deployed:", operator_success)
|
|
|
|
def is_crd_deployed(event):
|
|
expected = "sqlservers.mssql.microsoft.com"
|
|
if (event["type"] == "ADDED"
|
|
and event["object"].metadata.name == expected):
|
|
return True, event["object"]
|
|
else:
|
|
return False, event["object"]
|
|
|
|
extensions_api = kclient.ApiextensionsV1beta1Api()
|
|
|
|
# We need to sleep here.
|
|
# Kubernetes client API will raise the following exception if the operator
|
|
# is not fully deployed when trying to list_custom_resource_definition:
|
|
# File "kubernetes/client/models/v1beta1_custom_resource_definition_status.py", line 105, in conditions # noqa: E501
|
|
# raise ValueError("Invalid value for `conditions`, must not be `None`")
|
|
log(LogLevel.ALL, "Waiting 10 seconds...")
|
|
time.sleep(10)
|
|
|
|
# Wait for crd to be deployed.
|
|
log(LogLevel.ALL,
|
|
"Waiting for Custom Resource Definition to be initialized")
|
|
log(LogLevel.INFO,
|
|
"This can be slow during the first deployment because we need to",
|
|
"pull docker images")
|
|
kube_watch_event(is_crd_deployed,
|
|
extensions_api.list_custom_resource_definition)
|
|
|
|
return operator_success
|
|
|
|
|
|
def deploy_pv(namespace, sql_servers, pv_yaml):
|
|
pv_names = [get_pv_name(namespace, sql_name) for sql_name in sql_servers]
|
|
log(LogLevel.INFO, "Deploying Persistent Volumes")
|
|
kubectl(["apply", "-f", pv_yaml])
|
|
pvs_deployed = []
|
|
|
|
def all_pvs_deployed(event):
|
|
name = event["object"].metadata.name
|
|
if event["type"] == "ADDED" and name in pv_names:
|
|
pvs_deployed.append(name)
|
|
if (len(pv_names) == len(pvs_deployed)
|
|
and (sorted(pv_names) == sorted(pvs_deployed))):
|
|
return True, pvs_deployed
|
|
else:
|
|
return False, pvs_deployed
|
|
|
|
core_v1_api = kclient.CoreV1Api()
|
|
|
|
deployed_pvs = kube_watch_event(
|
|
all_pvs_deployed,
|
|
core_v1_api.list_persistent_volume,
|
|
timeout_seconds=60)
|
|
|
|
return deployed_pvs
|
|
|
|
|
|
def deploy_sqlservers(namespace, sql_servers, sqlserver_yaml):
|
|
log(LogLevel.INFO, "Deploying SQL Servers:", sql_servers)
|
|
kubectl(["apply", "-f", sqlserver_yaml])
|
|
sql_pods_deployed = []
|
|
|
|
def all_sql_deployed(event):
|
|
# Custom objects API returns dictionaries
|
|
name = event["object"]["metadata"]["name"]
|
|
if event["type"] == "ADDED" and name in sql_servers:
|
|
sql_pods_deployed.append(name)
|
|
if (len(sql_servers) == len(sql_pods_deployed)
|
|
and (sorted(sql_servers) == sorted(sql_pods_deployed))):
|
|
return True, sql_pods_deployed
|
|
else:
|
|
return False, sql_pods_deployed
|
|
|
|
custom_objects_api = kclient.CustomObjectsApi()
|
|
|
|
_, sql_pods_deployed = kube_watch_event(
|
|
all_sql_deployed,
|
|
custom_objects_api.list_namespaced_custom_object,
|
|
group="mssql.microsoft.com",
|
|
version="v1",
|
|
plural="sqlservers",
|
|
namespace=namespace)
|
|
|
|
return sql_pods_deployed
|
|
|
|
|
|
def deploy_ag_services(namespace, expected_names, ag_services_yaml):
|
|
log(LogLevel.INFO, "Deploying AG services in YAML:", ag_services_yaml)
|
|
kubectl(["apply", "-f", ag_services_yaml])
|
|
deployed_ag_services = []
|
|
|
|
def all_ag_services_deployed(event):
|
|
# service name
|
|
name = event["object"].metadata.name
|
|
if event["type"] == "ADDED" and name in expected_names:
|
|
deployed_ag_services.append(name)
|
|
|
|
if (len(expected_names) == len(deployed_ag_services)
|
|
and (sorted(expected_names) == sorted(deployed_ag_services))):
|
|
# all services have been deployed
|
|
return True, deployed_ag_services
|
|
else:
|
|
return False, deployed_ag_services
|
|
|
|
core_v1_api = kclient.CoreV1Api()
|
|
|
|
deployed_ag_services = kube_watch_event(
|
|
all_ag_services_deployed,
|
|
core_v1_api.list_namespaced_service,
|
|
namespace=namespace,
|
|
timeout_seconds=60)
|
|
|
|
return deployed_ag_services
|
|
|
|
|
|
def create_namespace(namespace):
|
|
log(LogLevel.INFO, "Creating namespace:", namespace)
|
|
kconfig.load_kube_config()
|
|
core_v1_api = kclient.CoreV1Api()
|
|
v1_ns = kclient.V1Namespace(metadata=kclient.V1ObjectMeta(name=namespace))
|
|
core_v1_api.create_namespace(body=v1_ns)
|
|
|
|
return v1_ns
|
|
|
|
|
|
class KubeWatchEventError(Exception):
|
|
pass
|
|
|
|
def kube_watch_event(completion_func, api_func, **kwargs):
|
|
watch = kwatch.Watch()
|
|
count = 8
|
|
completed = False
|
|
result = None
|
|
watch_timer = Timer(60.0, lambda: watch.stop())
|
|
|
|
ex_timer = Timer(300.0, KubeWatchEventError)
|
|
|
|
# watch.stream generator might throw an exception
|
|
# not sure how to handle.
|
|
watch_timer.start()
|
|
ex_timer.start()
|
|
try:
|
|
for event in watch.stream(api_func, **kwargs):
|
|
log(LogLevel.DEBUG, "Watch count:", count)
|
|
log(LogLevel.DEBUG, "Event:", event["type"], "object:",
|
|
event["object"])
|
|
count -= 1
|
|
completed, result = completion_func(event)
|
|
log(LogLevel.DEBUG, "Completed:", completed)
|
|
if completed:
|
|
watch.stop()
|
|
if not count:
|
|
watch.stop()
|
|
except KubeWatchEventError:
|
|
raise
|
|
except Exception as ex:
|
|
log(LogLevel.ERROR, "Caught exception:", str(ex))
|
|
watch.stop()
|
|
|
|
watch_timer.cancel()
|
|
ex_timer.cancel()
|
|
if not completed:
|
|
log(LogLevel.ERROR, str(api_func), "Could not complete sucessfully")
|
|
|
|
return completed, result
|
|
|
|
|
|
def apply_specs(namespace,
|
|
sql_servers,
|
|
ag_services,
|
|
operator_yaml_file=OPERATOR_YAML_FILENAME,
|
|
sql_secrets_yaml_file=SQL_SECRETS_YAML_FILENAME,
|
|
pv_yaml_file=PV_YAML_FILENAME,
|
|
sqlserver_yaml_file=SQLSERVER_YAML_FILENAME,
|
|
ag_services_yaml_file=AG_SERVICES_YAML_FILENAME):
|
|
kconfig.load_kube_config()
|
|
operator_deployed = deploy_operator(namespace, operator_yaml_file)
|
|
|
|
if operator_deployed:
|
|
log(LogLevel.ALL, "Successfully deployed mssql-operator")
|
|
log(LogLevel.ALL)
|
|
|
|
kubectl(["apply", "-f", sql_secrets_yaml_file])
|
|
|
|
if pv_yaml_file:
|
|
pvs_deployed = deploy_pv(namespace, sql_servers, pv_yaml_file)
|
|
log(LogLevel.ALL, "Successfully deployed Persistent Volumes",
|
|
pvs_deployed)
|
|
log(LogLevel.ALL)
|
|
|
|
sql_pods_deployed = (deploy_sqlservers(namespace, sql_servers,
|
|
sqlserver_yaml_file))
|
|
log(LogLevel.ALL, "Successfully deployed SQL pods", sql_pods_deployed)
|
|
log(LogLevel.ALL)
|
|
|
|
ag_services_deployed = (deploy_ag_services(namespace, ag_services,
|
|
ag_services_yaml_file))
|
|
|
|
log(LogLevel.ALL, "Successfully deployed AG services",
|
|
ag_services_deployed)
|
|
log(LogLevel.ALL)
|
|
|
|
|
|
def exit(exitcode):
|
|
color = TERM_GREEN if exitcode == 0 else TERM_RED
|
|
print(color, sys.argv[0], " exitcode: ", exitcode, TERM_END, sep="")
|
|
sys.exit(exitcode)
|
|
|
|
|
|
class ActionBase:
|
|
def __init__(self, parser):
|
|
self.parser = parser
|
|
self.init_parser()
|
|
self.parser.set_defaults(obj=self)
|
|
|
|
def parse_args(self):
|
|
args = self.parser.parse_args()
|
|
return args
|
|
|
|
def validate_args(self):
|
|
return True
|
|
|
|
def run(self, args, working_dir):
|
|
pass
|
|
|
|
def __str__(self):
|
|
return self.parser.prog
|
|
|
|
@classmethod
|
|
def add_to_parser(cls, parser):
|
|
return cls(parser)
|
|
|
|
|
|
class Environment(Enum):
|
|
ON_PREM = 0
|
|
AKS = 1
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
@staticmethod
|
|
def from_str_ignore_case(s):
|
|
try:
|
|
return Environment[s.upper()]
|
|
except KeyError:
|
|
raise ValueError()
|
|
|
|
@property
|
|
def service_annotations(self):
|
|
# if self is Environment.AKS:
|
|
# return {
|
|
# "service.beta.kubernetes.io/azure-load-balancer-internal":
|
|
# "true"
|
|
# }
|
|
# else:
|
|
return None
|
|
|
|
|
|
class DeployAction(ActionBase):
|
|
def init_parser(self):
|
|
parser = self.parser
|
|
parser.description = (
|
|
"Deploy SQL Server and k8s Agents in namespace(AG name)")
|
|
parser.add_argument(
|
|
"--ag",
|
|
default=DEFAULT_AG_NAME,
|
|
help="name of the Availability Group. Default=" + DEFAULT_AG_NAME)
|
|
parser.add_argument(
|
|
"-n",
|
|
"--namespace",
|
|
help=("name of the k8s namespace. " +
|
|
"Defaults to AG name if not specified."))
|
|
parser.add_argument(
|
|
"--dry-run",
|
|
action="store_true",
|
|
help="Perform a dry run and not apply the specs.")
|
|
parser.add_argument(
|
|
"-s",
|
|
"--sql-servers",
|
|
nargs="+",
|
|
default=DEFAULT_SQLSERVER_NAMES,
|
|
help="names of SQL Server instances(up to 5, separated by spaces)"
|
|
"Default=" + str(DEFAULT_SQLSERVER_NAMES))
|
|
default_sa_password = "SAPassword2018"
|
|
parser.add_argument(
|
|
"-p",
|
|
"--sa-password",
|
|
default=default_sa_password,
|
|
help="SA Password. Default='{}'".format(default_sa_password))
|
|
parser.add_argument(
|
|
"-e",
|
|
"--env",
|
|
type=Environment.from_str_ignore_case,
|
|
choices=list(Environment),
|
|
default=Environment.ON_PREM)
|
|
|
|
parser.add_argument(
|
|
"--skip-create-namespace",
|
|
action="store_true",
|
|
help="Skip namespace creation.")
|
|
|
|
def validate_args(self, args):
|
|
args.sql_servers.sort()
|
|
sql_list = []
|
|
for sql in args.sql_servers:
|
|
if len(sql_list) > 0 and sql_list[-1] == sql:
|
|
log(LogLevel.WARNING, "duplicate SQL Server = ", sql)
|
|
else:
|
|
sql_list.append(sql)
|
|
|
|
if not (0 < len(sql_list) <= 5):
|
|
log(LogLevel.ERROR, "Invalid number of sql_servers =", sql_list,
|
|
"size =", len(sql_list))
|
|
log(LogLevel.Error, "Expected 0 < number of SQL Server <= 5")
|
|
return False
|
|
|
|
return True
|
|
|
|
def run(self, args, working_dir):
|
|
exitcode = 0
|
|
|
|
log(LogLevel.INFO, "Working directory:", working_dir)
|
|
log(LogLevel.INFO)
|
|
|
|
operator_yaml_path = os.path.join(working_dir, OPERATOR_YAML_FILENAME)
|
|
sql_secrets_yaml_path = os.path.join(working_dir,
|
|
SQL_SECRETS_YAML_FILENAME)
|
|
pv_yaml_path = None
|
|
sqlservers_yaml_path = os.path.join(working_dir,
|
|
SQLSERVER_YAML_FILENAME)
|
|
ag_services_yaml_path = os.path.join(working_dir,
|
|
AG_SERVICES_YAML_FILENAME)
|
|
|
|
create_operator_yaml(
|
|
args.namespace,
|
|
DEFAULT_K8S_AGENTS_IMAGE,
|
|
filepath=operator_yaml_path)
|
|
create_sql_secrets_yaml(
|
|
args.namespace, args.sa_password, filepath=sql_secrets_yaml_path)
|
|
|
|
if args.env is Environment.ON_PREM:
|
|
pv_yaml_path = os.path.join(working_dir, PV_YAML_FILENAME)
|
|
pv_yaml_list = create_pv_yaml(
|
|
args.namespace,
|
|
args.sql_servers,
|
|
root_path=working_dir,
|
|
filepath=pv_yaml_path)
|
|
|
|
create_sqlservers_yaml(
|
|
args.env,
|
|
args.namespace,
|
|
args.sql_servers, [args.ag],
|
|
args.sa_password,
|
|
DEFAULT_K8S_AGENTS_IMAGE,
|
|
filepath=sqlservers_yaml_path)
|
|
load_balancer_services = create_ag_services_yaml(
|
|
args.env,
|
|
args.namespace, args.ag, filepath=ag_services_yaml_path)
|
|
|
|
if not args.dry_run:
|
|
if not args.skip_create_namespace:
|
|
create_namespace(args.namespace)
|
|
|
|
log(LogLevel.ALL)
|
|
if args.env is Environment.ON_PREM:
|
|
for pv_yaml in pv_yaml_list:
|
|
log(LogLevel.ALL, "Persistent Volume:", pv_yaml.get_name(),
|
|
"mkdir:", pv_yaml.get_path())
|
|
os.mkdir(pv_yaml.get_path())
|
|
|
|
expected_service_names = [
|
|
service_yaml.name for service_yaml in load_balancer_services
|
|
]
|
|
apply_specs(args.namespace, args.sql_servers,
|
|
expected_service_names, operator_yaml_path,
|
|
sql_secrets_yaml_path, pv_yaml_path,
|
|
sqlservers_yaml_path, ag_services_yaml_path)
|
|
|
|
paths = [
|
|
operator_yaml_path, sql_secrets_yaml_path, pv_yaml_path,
|
|
sqlservers_yaml_path, ag_services_yaml_path
|
|
]
|
|
paths = [path for path in paths if path]
|
|
return (exitcode, paths)
|
|
|
|
|
|
class FailoverAction(ActionBase):
|
|
def init_parser(self):
|
|
parser = self.parser
|
|
parser.description = "Perform a manual failover"
|
|
parser.add_argument(
|
|
"target_replica",
|
|
help="name of target SQL Server replica to failover to")
|
|
parser.add_argument(
|
|
"--ag",
|
|
default=DEFAULT_AG_NAME,
|
|
help="name of the Availability Group. Default=" + DEFAULT_AG_NAME)
|
|
parser.add_argument(
|
|
"--namespace",
|
|
help=("name of the k8s namespace. " +
|
|
"Defaults to AG name if not specified"))
|
|
parser.add_argument(
|
|
"--dry-run",
|
|
action="store_true",
|
|
help="Perform a dry run and NOT apply the specs")
|
|
# parser.add_argument(
|
|
# "--force-failover-allow-data-loss",
|
|
# action="store_true",
|
|
# help="Perform a force failover allow data loss")
|
|
|
|
def validate_args(self, args):
|
|
"""
|
|
Validate the target replica exists?
|
|
"""
|
|
return True
|
|
|
|
def run(self, args, working_dir):
|
|
exitcode = 0
|
|
failover_yaml_path = os.path.join(working_dir, FAILOVER_YAML_FILENAME)
|
|
failover_yaml = create_failover_yaml(
|
|
namespace=args.namespace,
|
|
agent_image=DEFAULT_K8S_AGENTS_IMAGE,
|
|
ag=args.ag,
|
|
target_replica=args.target_replica,
|
|
filepath=failover_yaml_path)
|
|
if not args.dry_run:
|
|
log(LogLevel.ALL, "Creating failover job")
|
|
proc = kubectl(["apply", "-f", failover_yaml_path])
|
|
log(LogLevel.ALL, "Failover job created")
|
|
exitcode = proc.returncode
|
|
return (exitcode, [failover_yaml_path])
|
|
|
|
|
|
def main():
|
|
parent_parser = ArgumentParser(add_help=False)
|
|
parent_parser.add_argument(
|
|
"--verbose",
|
|
"-v",
|
|
default=LogLevel.ERROR,
|
|
action="count",
|
|
help="Verbosity of output")
|
|
root_parser = ArgumentParser()
|
|
subparsers = root_parser.add_subparsers(
|
|
description="Actions on k8s agent", dest="subaction")
|
|
DeployAction.add_to_parser(
|
|
subparsers.add_parser(
|
|
"deploy",
|
|
help="Deploy a set of SQL Servers in an Availability Group",
|
|
parents=[parent_parser]))
|
|
FailoverAction.add_to_parser(
|
|
subparsers.add_parser(
|
|
"failover",
|
|
help="Perform a failover to a target replica.",
|
|
parents=[parent_parser]))
|
|
|
|
args = root_parser.parse_args()
|
|
# namespace defaults to AG name
|
|
if args.namespace is None or args.namespace == "":
|
|
args.namespace = args.ag
|
|
|
|
log(LogLevel.INFO, "args:", vars(args))
|
|
|
|
if not args.subaction:
|
|
root_parser.print_help()
|
|
exit(1)
|
|
|
|
global log_verbosity
|
|
log_verbosity = args.verbose
|
|
|
|
if not args.obj.validate_args(args):
|
|
print("action.validate_args failed:", args.obj, args)
|
|
root_parser.print_help()
|
|
exit(1)
|
|
|
|
log(LogLevel.INFO, sys.argv[0], "Startup Time:", datetime.now(), "UTC:",
|
|
datetime.utcnow())
|
|
log(LogLevel.INFO)
|
|
|
|
# create temp directory
|
|
working_dir = mkdtemp(
|
|
prefix="kube_agent_{}-".format(args.subaction),
|
|
suffix=(args.namespace))
|
|
|
|
# Run action here
|
|
exitcode, spec_paths = args.obj.run(args, working_dir)
|
|
|
|
log(LogLevel.INFO, sys.argv[0], "Completion Time:", datetime.now(), "UTC:",
|
|
datetime.utcnow())
|
|
log(LogLevel.INFO, "----------")
|
|
if spec_paths:
|
|
log(LogLevel.ALL, "Created the following specs:")
|
|
for path in spec_paths:
|
|
log(LogLevel.ALL, "\t", path)
|
|
log(LogLevel.ALL)
|
|
try:
|
|
specs_file = "{}_{}_specs".format(args.subaction, args.namespace)
|
|
log(LogLevel.ALL, "Wrote spec paths:", "'{}'".format(specs_file))
|
|
with open(specs_file, "w") as f:
|
|
f.write("\n".join(spec_paths) + "\n")
|
|
except IOError as ex:
|
|
log(LogLevel.ALL, "Caught IOError:", ex, "writing to",
|
|
"'{}'".format(specs_file))
|
|
exit(exitcode)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|