{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "CER020 - Create Management Proxy certificate\n", "============================================\n", "\n", "This notebook creates a certificate for the Management Proxy endpoint.\n", "\n", "Steps\n", "-----\n", "\n", "### Parameters" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [ "parameters" ] }, "outputs": [], "source": [ "import getpass\n", "\n", "app_name = \"mgmtproxy\"\n", "scaledset_name = \"mgmtproxy\"\n", "container_name = \"service-proxy\"\n", "prefix_keyfile_name = \"service-proxy\"\n", "common_name = \"mgmtproxy-svc\"\n", "\n", "country_name = \"US\"\n", "state_or_province_name = \"Illinois\"\n", "locality_name = \"Chicago\"\n", "organization_name = \"Contoso\"\n", "organizational_unit_name = \"Finance\"\n", "email_address = f\"{getpass.getuser()}@contoso.com\"\n", "\n", "ssl_configuration_file = \"service.openssl.cnf\"\n", "\n", "days = \"825\" # the number of days to certify the certificate for\n", "\n", "test_cert_store_root = \"/var/opt/secrets/test-certificates\"\n", "\n", "extendedKeyUsage = \"\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Common functions\n", "\n", "Define helper functions used in this notebook." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [ "hide_input" ] }, "outputs": [], "source": [ "# Define `run` function for transient fault handling, suggestions on error, and scrolling updates on Windows\n", "import sys\n", "import os\n", "import re\n", "import json\n", "import platform\n", "import shlex\n", "import shutil\n", "import datetime\n", "\n", "from subprocess import Popen, PIPE\n", "from IPython.display import Markdown\n", "\n", "retry_hints = {} # Output in stderr known to be transient, therefore automatically retry\n", "error_hints = {} # Output in stderr where a known SOP/TSG exists which will be HINTed for further help\n", "install_hint = {} # The SOP to help install the executable if it cannot be found\n", "\n", "first_run = True\n", "rules = None\n", "debug_logging = False\n", "\n", "def run(cmd, return_output=False, no_output=False, retry_count=0):\n", " \"\"\"Run shell command, stream stdout, print stderr and optionally return output\n", "\n", " NOTES:\n", "\n", " 1. Commands that need this kind of ' quoting on Windows e.g.:\n", "\n", " kubectl get nodes -o jsonpath={.items[?(@.metadata.annotations.pv-candidate=='data-pool')].metadata.name}\n", "\n", " Need to actually pass in as '\"':\n", "\n", " kubectl get nodes -o jsonpath={.items[?(@.metadata.annotations.pv-candidate=='\"'data-pool'\"')].metadata.name}\n", "\n", " The ' quote approach, although correct when pasting into Windows cmd, will hang at the line:\n", " \n", " `iter(p.stdout.readline, b'')`\n", "\n", " The shlex.split call does the right thing for each platform, just use the '\"' pattern for a '\n", " \"\"\"\n", " MAX_RETRIES = 5\n", " output = \"\"\n", " retry = False\n", "\n", " global first_run\n", " global rules\n", "\n", " if first_run:\n", " first_run = False\n", " rules = load_rules()\n", "\n", " # When running `azdata sql query` on Windows, replace any \\n in \"\"\" strings, with \" \", otherwise we see:\n", " #\n", " # ('HY090', '[HY090] [Microsoft][ODBC Driver Manager] Invalid string or buffer length (0) (SQLExecDirectW)')\n", " #\n", " if platform.system() == \"Windows\" and cmd.startswith(\"azdata sql query\"):\n", " cmd = cmd.replace(\"\\n\", \" \")\n", "\n", " # shlex.split is required on bash and for Windows paths with spaces\n", " #\n", " cmd_actual = shlex.split(cmd)\n", "\n", " # Store this (i.e. kubectl, python etc.) to support binary context aware error_hints and retries\n", " #\n", " user_provided_exe_name = cmd_actual[0].lower()\n", "\n", " # When running python, use the python in the ADS sandbox ({sys.executable})\n", " #\n", " if cmd.startswith(\"python \"):\n", " cmd_actual[0] = cmd_actual[0].replace(\"python\", sys.executable)\n", "\n", " # On Mac, when ADS is not launched from terminal, LC_ALL may not be set, which causes pip installs to fail\n", " # with:\n", " #\n", " # UnicodeDecodeError: 'ascii' codec can't decode byte 0xc5 in position 4969: ordinal not in range(128)\n", " #\n", " # Setting it to a default value of \"en_US.UTF-8\" enables pip install to complete\n", " #\n", " if platform.system() == \"Darwin\" and \"LC_ALL\" not in os.environ:\n", " os.environ[\"LC_ALL\"] = \"en_US.UTF-8\"\n", "\n", " # When running `kubectl`, if AZDATA_OPENSHIFT is set, use `oc`\n", " #\n", " if cmd.startswith(\"kubectl \") and \"AZDATA_OPENSHIFT\" in os.environ:\n", " cmd_actual[0] = cmd_actual[0].replace(\"kubectl\", \"oc\")\n", "\n", " # To aid supportabilty, determine which binary file will actually be executed on the machine\n", " #\n", " which_binary = None\n", "\n", " # Special case for CURL on Windows. The version of CURL in Windows System32 does not work to\n", " # get JWT tokens, it returns \"(56) Failure when receiving data from the peer\". If another instance\n", " # of CURL exists on the machine use that one. (Unfortunately the curl.exe in System32 is almost\n", " # always the first curl.exe in the path, and it can't be uninstalled from System32, so here we\n", " # look for the 2nd installation of CURL in the path)\n", " if platform.system() == \"Windows\" and cmd.startswith(\"curl \"):\n", " path = os.getenv('PATH')\n", " for p in path.split(os.path.pathsep):\n", " p = os.path.join(p, \"curl.exe\")\n", " if os.path.exists(p) and os.access(p, os.X_OK):\n", " if p.lower().find(\"system32\") == -1:\n", " cmd_actual[0] = p\n", " which_binary = p\n", " break\n", "\n", " # Find the path based location (shutil.which) of the executable that will be run (and display it to aid supportability), this\n", " # seems to be required for .msi installs of azdata.cmd/az.cmd. (otherwise Popen returns FileNotFound) \n", " #\n", " # NOTE: Bash needs cmd to be the list of the space separated values hence shlex.split.\n", " #\n", " if which_binary == None:\n", " which_binary = shutil.which(cmd_actual[0])\n", "\n", " if which_binary == None:\n", " if user_provided_exe_name in install_hint and install_hint[user_provided_exe_name] is not None:\n", " display(Markdown(f'HINT: Use [{install_hint[user_provided_exe_name][0]}]({install_hint[user_provided_exe_name][1]}) to resolve this issue.'))\n", "\n", " raise FileNotFoundError(f\"Executable '{cmd_actual[0]}' not found in path (where/which)\")\n", " else: \n", " cmd_actual[0] = which_binary\n", "\n", " start_time = datetime.datetime.now().replace(microsecond=0)\n", "\n", " print(f\"START: {cmd} @ {start_time} ({datetime.datetime.utcnow().replace(microsecond=0)} UTC)\")\n", " print(f\" using: {which_binary} ({platform.system()} {platform.release()} on {platform.machine()})\")\n", " print(f\" cwd: {os.getcwd()}\")\n", "\n", " # Command-line tools such as CURL and AZDATA HDFS commands output\n", " # scrolling progress bars, which causes Jupyter to hang forever, to\n", " # workaround this, use no_output=True\n", " #\n", "\n", " # Work around a infinite hang when a notebook generates a non-zero return code, break out, and do not wait\n", " #\n", " wait = True \n", "\n", " try:\n", " if no_output:\n", " p = Popen(cmd_actual)\n", " else:\n", " p = Popen(cmd_actual, stdout=PIPE, stderr=PIPE, bufsize=1)\n", " with p.stdout:\n", " for line in iter(p.stdout.readline, b''):\n", " line = line.decode()\n", " if return_output:\n", " output = output + line\n", " else:\n", " if cmd.startswith(\"azdata notebook run\"): # Hyperlink the .ipynb file\n", " regex = re.compile(' \"(.*)\"\\: \"(.*)\"') \n", " match = regex.match(line)\n", " if match:\n", " if match.group(1).find(\"HTML\") != -1:\n", " display(Markdown(f' - \"{match.group(1)}\": \"{match.group(2)}\"'))\n", " else:\n", " display(Markdown(f' - \"{match.group(1)}\": \"[{match.group(2)}]({match.group(2)})\"'))\n", "\n", " wait = False\n", " break # otherwise infinite hang, have not worked out why yet.\n", " else:\n", " print(line, end='')\n", " if rules is not None:\n", " apply_expert_rules(line)\n", "\n", " if wait:\n", " p.wait()\n", " except FileNotFoundError as e:\n", " if install_hint is not None:\n", " display(Markdown(f'HINT: Use {install_hint} to resolve this issue.'))\n", "\n", " raise FileNotFoundError(f\"Executable '{cmd_actual[0]}' not found in path (where/which)\") from e\n", "\n", " exit_code_workaround = 0 # WORKAROUND: azdata hangs on exception from notebook on p.wait()\n", "\n", " if not no_output:\n", " for line in iter(p.stderr.readline, b''):\n", " try:\n", " line_decoded = line.decode()\n", " except UnicodeDecodeError:\n", " # NOTE: Sometimes we get characters back that cannot be decoded(), e.g.\n", " #\n", " # \\xa0\n", " #\n", " # For example see this in the response from `az group create`:\n", " #\n", " # ERROR: Get Token request returned http error: 400 and server \n", " # response: {\"error\":\"invalid_grant\",# \"error_description\":\"AADSTS700082: \n", " # The refresh token has expired due to inactivity.\\xa0The token was \n", " # issued on 2018-10-25T23:35:11.9832872Z\n", " #\n", " # which generates the exception:\n", " #\n", " # UnicodeDecodeError: 'utf-8' codec can't decode byte 0xa0 in position 179: invalid start byte\n", " #\n", " print(\"WARNING: Unable to decode stderr line, printing raw bytes:\")\n", " print(line)\n", " line_decoded = \"\"\n", " pass\n", " else:\n", "\n", " # azdata emits a single empty line to stderr when doing an hdfs cp, don't\n", " # print this empty \"ERR:\" as it confuses.\n", " #\n", " if line_decoded == \"\":\n", " continue\n", " \n", " print(f\"STDERR: {line_decoded}\", end='')\n", "\n", " if line_decoded.startswith(\"An exception has occurred\") or line_decoded.startswith(\"ERROR: An error occurred while executing the following cell\"):\n", " exit_code_workaround = 1\n", "\n", " # inject HINTs to next TSG/SOP based on output in stderr\n", " #\n", " if user_provided_exe_name in error_hints:\n", " for error_hint in error_hints[user_provided_exe_name]:\n", " if line_decoded.find(error_hint[0]) != -1:\n", " display(Markdown(f'HINT: Use [{error_hint[1]}]({error_hint[2]}) to resolve this issue.'))\n", "\n", " # apply expert rules (to run follow-on notebooks), based on output\n", " #\n", " if rules is not None:\n", " apply_expert_rules(line_decoded)\n", "\n", " # Verify if a transient error, if so automatically retry (recursive)\n", " #\n", " if user_provided_exe_name in retry_hints:\n", " for retry_hint in retry_hints[user_provided_exe_name]:\n", " if line_decoded.find(retry_hint) != -1:\n", " if retry_count < MAX_RETRIES:\n", " print(f\"RETRY: {retry_count} (due to: {retry_hint})\")\n", " retry_count = retry_count + 1\n", " output = run(cmd, return_output=return_output, retry_count=retry_count)\n", "\n", " if return_output:\n", " return output\n", " else:\n", " return\n", "\n", " elapsed = datetime.datetime.now().replace(microsecond=0) - start_time\n", "\n", " # WORKAROUND: We avoid infinite hang above in the `azdata notebook run` failure case, by inferring success (from stdout output), so\n", " # don't wait here, if success known above\n", " #\n", " if wait: \n", " if p.returncode != 0:\n", " raise SystemExit(f'Shell command:\\n\\n\\t{cmd} ({elapsed}s elapsed)\\n\\nreturned non-zero exit code: {str(p.returncode)}.\\n')\n", " else:\n", " if exit_code_workaround !=0 :\n", " raise SystemExit(f'Shell command:\\n\\n\\t{cmd} ({elapsed}s elapsed)\\n\\nreturned non-zero exit code: {str(exit_code_workaround)}.\\n')\n", "\n", " print(f'\\nSUCCESS: {elapsed}s elapsed.\\n')\n", "\n", " if return_output:\n", " return output\n", "\n", "def load_json(filename):\n", " \"\"\"Load a json file from disk and return the contents\"\"\"\n", "\n", " with open(filename, encoding=\"utf8\") as json_file:\n", " return json.load(json_file)\n", "\n", "def load_rules():\n", " \"\"\"Load any 'expert rules' from the metadata of this notebook (.ipynb) that should be applied to the stderr of the running executable\"\"\"\n", "\n", " try:\n", "\n", " # Load this notebook as json to get access to the expert rules in the notebook metadata.\n", " #\n", " j = load_json(\"cer020-create-management-service-proxy-cert.ipynb\")\n", "\n", " except:\n", " pass # If the user has renamed the book, we can't load ourself. NOTE: Is there a way in Jupyter, to know your own filename?\n", "\n", " else:\n", " if \"metadata\" in j and \\\n", " \"azdata\" in j[\"metadata\"] and \\\n", " \"expert\" in j[\"metadata\"][\"azdata\"] and \\\n", " \"rules\" in j[\"metadata\"][\"azdata\"][\"expert\"]:\n", "\n", " rules = j[\"metadata\"][\"azdata\"][\"expert\"][\"rules\"]\n", "\n", " rules.sort() # Sort rules, so they run in priority order (the [0] element). Lowest value first.\n", "\n", " # print (f\"EXPERT: There are {len(rules)} rules to evaluate.\")\n", "\n", " return rules\n", "\n", "def apply_expert_rules(line):\n", " \"\"\"Determine if the stderr line passed in, matches the regular expressions for any of the 'expert rules', if so\n", " inject a 'HINT' to the follow-on SOP/TSG to run\"\"\"\n", "\n", " global rules\n", "\n", " for rule in rules:\n", "\n", " # rules that have 9 elements are the injected (output) rules (the ones we want). Rules\n", " # with only 8 elements are the source (input) rules, which are not expanded (i.e. TSG029,\n", " # not ../repair/tsg029-nb-name.ipynb)\n", " if len(rule) == 9:\n", " notebook = rule[1]\n", " cell_type = rule[2]\n", " output_type = rule[3] # i.e. stream or error\n", " output_type_name = rule[4] # i.e. ename or name \n", " output_type_value = rule[5] # i.e. SystemExit or stdout\n", " details_name = rule[6] # i.e. evalue or text \n", " expression = rule[7].replace(\"\\\\*\", \"*\") # Something escaped *, and put a \\ in front of it!\n", "\n", " if debug_logging:\n", " print(f\"EXPERT: If rule '{expression}' satisfied', run '{notebook}'.\")\n", "\n", " if re.match(expression, line, re.DOTALL):\n", "\n", " if debug_logging:\n", " print(\"EXPERT: MATCH: name = value: '{0}' = '{1}' matched expression '{2}', therefore HINT '{4}'\".format(output_type_name, output_type_value, expression, notebook))\n", "\n", " match_found = True\n", "\n", " display(Markdown(f'HINT: Use [{notebook}]({notebook}) to resolve this issue.'))\n", "\n", "\n", "\n", "\n", "print('Common functions defined successfully.')\n", "\n", "# Hints for binary (transient fault) retry, (known) error and install guide\n", "#\n", "retry_hints = {'kubectl': ['A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond'], 'azdata': ['Endpoint sql-server-master does not exist', 'Endpoint livy does not exist', 'Failed to get state for cluster', 'Endpoint webhdfs does not exist', 'Adaptive Server is unavailable or does not exist', 'Error: Address already in use']}\n", "error_hints = {'kubectl': [['no such host', 'TSG010 - Get configuration contexts', '../monitor-k8s/tsg010-get-kubernetes-contexts.ipynb'], ['no such host', 'TSG011 - Restart sparkhistory server', '../repair/tsg011-restart-sparkhistory-server.ipynb'], ['No connection could be made because the target machine actively refused it', 'TSG056 - Kubectl fails with No connection could be made because the target machine actively refused it', '../repair/tsg056-kubectl-no-connection-could-be-made.ipynb']], 'azdata': [['azdata login', 'SOP028 - azdata login', '../common/sop028-azdata-login.ipynb'], ['The token is expired', 'SOP028 - azdata login', '../common/sop028-azdata-login.ipynb'], ['Reason: Unauthorized', 'SOP028 - azdata login', '../common/sop028-azdata-login.ipynb'], ['Max retries exceeded with url: /api/v1/bdc/endpoints', 'SOP028 - azdata login', '../common/sop028-azdata-login.ipynb'], ['Look at the controller logs for more details', 'TSG027 - Observe cluster deployment', '../diagnose/tsg027-observe-bdc-create.ipynb'], ['provided port is already allocated', 'TSG062 - Get tail of all previous container logs for pods in BDC namespace', '../log-files/tsg062-tail-bdc-previous-container-logs.ipynb'], ['Create cluster failed since the existing namespace', 'SOP061 - Delete a big data cluster', '../install/sop061-delete-bdc.ipynb'], ['Failed to complete kube config setup', 'TSG067 - Failed to complete kube config setup', '../repair/tsg067-failed-to-complete-kube-config-setup.ipynb'], ['Error processing command: \"ApiError', 'TSG110 - Azdata returns ApiError', '../repair/tsg110-azdata-returns-apierror.ipynb'], ['Error processing command: \"ControllerError', 'TSG036 - Controller logs', '../log-analyzers/tsg036-get-controller-logs.ipynb'], ['ERROR: 500', 'TSG046 - Knox gateway logs', '../log-analyzers/tsg046-get-knox-logs.ipynb'], ['Data source name not found and no default driver specified', 'SOP069 - Install ODBC for SQL Server', '../install/sop069-install-odbc-driver-for-sql-server.ipynb'], [\"Can't open lib 'ODBC Driver 17 for SQL Server\", 'SOP069 - Install ODBC for SQL Server', '../install/sop069-install-odbc-driver-for-sql-server.ipynb'], ['Control plane upgrade failed. Failed to upgrade controller.', 'TSG108 - View the controller upgrade config map', '../diagnose/tsg108-controller-failed-to-upgrade.ipynb']]}\n", "install_hint = {'kubectl': ['SOP036 - Install kubectl command line interface', '../install/sop036-install-kubectl.ipynb'], 'azdata': ['SOP063 - Install azdata CLI (using package manager)', '../install/sop063-packman-install-azdata.ipynb']}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Get the Kubernetes namespace for the big data cluster\n", "\n", "Get the namespace of the Big Data Cluster use the kubectl command line\n", "interface .\n", "\n", "**NOTE:**\n", "\n", "If there is more than one Big Data Cluster in the target Kubernetes\n", "cluster, then either:\n", "\n", "- set \\[0\\] to the correct value for the big data cluster.\n", "- set the environment variable AZDATA\\_NAMESPACE, before starting\n", " Azure Data Studio." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [ "hide_input" ] }, "outputs": [], "source": [ "# Place Kubernetes namespace name for BDC into 'namespace' variable\n", "\n", "if \"AZDATA_NAMESPACE\" in os.environ:\n", " namespace = os.environ[\"AZDATA_NAMESPACE\"]\n", "else:\n", " try:\n", " namespace = run(f'kubectl get namespace --selector=MSSQL_CLUSTER -o jsonpath={{.items[0].metadata.name}}', return_output=True)\n", " except:\n", " from IPython.display import Markdown\n", " print(f\"ERROR: Unable to find a Kubernetes namespace with label 'MSSQL_CLUSTER'. SQL Server Big Data Cluster Kubernetes namespaces contain the label 'MSSQL_CLUSTER'.\")\n", " display(Markdown(f'HINT: Use [TSG081 - Get namespaces (Kubernetes)](../monitor-k8s/tsg081-get-kubernetes-namespaces.ipynb) to resolve this issue.'))\n", " display(Markdown(f'HINT: Use [TSG010 - Get configuration contexts](../monitor-k8s/tsg010-get-kubernetes-contexts.ipynb) to resolve this issue.'))\n", " display(Markdown(f'HINT: Use [SOP011 - Set kubernetes configuration context](../common/sop011-set-kubernetes-context.ipynb) to resolve this issue.'))\n", " raise\n", "\n", "print(f'The SQL Server Big Data Cluster Kubernetes namespace is: {namespace}')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Create a temporary directory to stage files" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [ "hide_input" ] }, "outputs": [], "source": [ "# Create a temporary directory to hold configuration files\n", "\n", "import tempfile\n", "\n", "temp_dir = tempfile.mkdtemp()\n", "\n", "print(f\"Temporary directory created: {temp_dir}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Helper function to save configuration files to disk" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [ "hide_input" ] }, "outputs": [], "source": [ "# Define helper function 'save_file' to save configuration files to the temporary directory created above\n", "import os\n", "import io\n", "\n", "def save_file(filename, contents):\n", " with io.open(os.path.join(temp_dir, filename), \"w\", encoding='utf8', newline='\\n') as text_file:\n", " text_file.write(contents)\n", "\n", " print(\"File saved: \" + os.path.join(temp_dir, filename))\n", "\n", "print(\"Function `save_file` defined successfully.\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Get endpoint hostname" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import json\n", "import urllib\n", "\n", "endpoint = run(f'azdata bdc endpoint list --endpoint=\"{app_name}\"', return_output=True)\n", "endpoint = json.loads(endpoint)\n", "endpoint = endpoint['endpoint']\n", "\n", "print(f\"endpoint: {endpoint}\")\n", "\n", "hostname = urllib.parse.urlparse(endpoint).hostname\n", "\n", "print(f\"hostname: {hostname}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Get name of the \u2018Running\u2019 `controller` `pod`" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [ "hide_input" ] }, "outputs": [], "source": [ "# Place the name of the 'Running' controller pod in variable `controller`\n", "\n", "controller = run(f'kubectl get pod --selector=app=controller -n {namespace} -o jsonpath={{.items[0].metadata.name}} --field-selector=status.phase=Running', return_output=True)\n", "\n", "print(f\"Controller pod name: {controller}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Create the DNS alt\\_names for data plane in secure clusters\n", "\n", "Get the cluster configuration from the Big Data Cluster using\n", "`azdata bdc config`, and pull the Active Directory DNS names out of it,\n", "and place them into the certificate configuration file as DNS alt\\_names" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import json\n", "\n", "alt_names = \"\"\n", "\n", "bdc_config = run(\"azdata bdc config show\", return_output=True)\n", "bdc_config = json.loads(bdc_config)\n", "\n", "dns_counter = 3 # DNS.1 and DNS.2 are already in the certificate template.\n", "\n", "# Add entry for \"DNS.3 = {common_name}..local\"\n", "#\n", "if \"security\" in bdc_config[\"spec\"] and \"activeDirectory\" in bdc_config[\"spec\"][\"security\"]:\n", " domain_dns_name = bdc_config[\"spec\"][\"security\"][\"activeDirectory\"][\"domainDnsName\"]\n", "\n", " alt_names += f\"DNS.{str(dns_counter)} = {common_name}.{domain_dns_name}\\n\"\n", " dns_counter = dns_counter + 1\n", "\n", "# Add entry for \"DNS.4 = ..local\"\n", "#\n", "if app_name in bdc_config[\"spec\"][\"resources\"]:\n", " app_name_endpoints = bdc_config[\"spec\"][\"resources\"][app_name][\"spec\"][\"endpoints\"]\n", " for endpoint in app_name_endpoints:\n", " if \"dnsName\" in endpoint:\n", " alt_names += f'DNS.{str(dns_counter)} = {endpoint[\"dnsName\"]}\\n'\n", " dns_counter = dns_counter + 1\n", "\n", "# Special case for the controller certificate\n", "#\n", "if app_name == \"controller\":\n", " alt_names += f\"DNS.{str(dns_counter)} = localhost\\n\"\n", " dns_counter = dns_counter + 1\n", "\n", "print(\"DNS alt_names (data plane):\")\n", "print(alt_names)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Create the DNS alt\\_names for control plane in secure clusters\n", "\n", "Get the cluster configuration from the Big Data Cluster using\n", "`azdata bdc endpoint list`, and pull the Active Directory DNS names out\n", "of it for the control plane expternal endpoints (Controller and\n", "Management Proxy), and place them into the certificate configuration\n", "file as DNS alt\\_names" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import json\n", "from urllib.parse import urlparse\n", "\n", "if app_name == \"controller\" or app_name == \"mgmtproxy\":\n", " bdc_endpoint_list = run(\"azdata bdc endpoint list\", return_output=True)\n", " bdc_endpoint_list = json.loads(bdc_endpoint_list)\n", "\n", " # Parse the DNS host name from:\n", " #\n", " # \"endpoint\": \"https://monitor.aris.local:30777\"\n", " # \n", " for endpoint in bdc_endpoint_list:\n", " if endpoint[\"name\"] == app_name:\n", " url = urlparse(endpoint[\"endpoint\"])\n", " alt_names += f\"DNS.{str(dns_counter)} = {url.hostname}\\n\"\n", " dns_counter = dns_counter + 1\n", "\n", "print(\"DNS alt_names (control plane):\")\n", "print(alt_names)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Create alt\\_names\n", "\n", "If the Kuberenetes service is of \u201cNodePort\u201d type, then the IP address\n", "needed to validate the cluster certificate could be for any node in the\n", "Kubernetes cluster, so here all node IP addresses in the Big Data\n", "Cluster are added as alt\\_names. Otherwise (if not NodePort, and\n", "therefore LoadBalancer), add just the hostname as returned from\n", "`azdata bdc endpoint list` above." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "service_type = run(f\"kubectl get svc {common_name}-external -n {namespace} -o jsonpath={{.spec.type}}\", return_output=True)\n", "\n", "print(f\"Service type for '{common_name}-external' is: '{service_type}'\")\n", "print(\"\")\n", "\n", "if service_type == \"NodePort\":\n", " nodes_ip_address = run(\"kubectl \"\"get nodes -o jsonpath={.items[*].status.addresses[0].address}\"\"\", return_output=True)\n", " nodes_ip_address = nodes_ip_address.split(' ')\n", "\n", " counter = 1\n", " for ip in nodes_ip_address:\n", " alt_names += f\"IP.{counter} = {ip}\\n\"\n", " counter = counter + 1\n", "else:\n", " alt_names += f\"IP.1 = {hostname}\\n\"\n", "\n", "print(\"All (DNS and IP) alt_names:\")\n", "print(alt_names)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Generate Certificate Configuration file\n", "\n", "NOTE: There is a special case for the `controller` certificate, that\n", "needs to be generated in PKCS\\#1 format." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "certificate = f\"\"\"\n", "[ req ]\n", "# Options for the `req` tool (`man req`).\n", "default_bits = 2048\n", "default_keyfile = {test_cert_store_root}/{app_name}/{prefix_keyfile_name}-privatekey{\".pkcs8\" if app_name == \"controller\" else \"\"}.pem\n", "distinguished_name = req_distinguished_name\n", "string_mask = utf8only\n", "\n", "# SHA-1 is deprecated, so use SHA-2 instead.\n", "default_md = sha256\n", "req_extensions = v3_req\n", "\n", "[ req_distinguished_name ]\n", "countryName = Country Name (2 letter code)\n", "countryName_default = {country_name}\n", "\n", "stateOrProvinceName = State or Province Name (full name)\n", "stateOrProvinceName_default = {state_or_province_name}\n", "\n", "localityName = Locality Name (eg, city)\n", "localityName_default = {locality_name}\n", "\n", "organizationName = Organization Name (eg, company)\n", "organizationName_default = {organization_name}\n", "\n", "organizationalUnitName = Organizational Unit (eg, division)\n", "organizationalUnitName_default = {organizational_unit_name}\n", "\n", "commonName = Common Name (e.g. server FQDN or YOUR name)\n", "commonName_default = {common_name}\n", "\n", "emailAddress = Email Address\n", "emailAddress_default = {email_address}\n", "\n", "[ v3_req ]\n", "subjectAltName = @alt_names\n", "subjectKeyIdentifier = hash\n", "basicConstraints = CA:FALSE\n", "keyUsage = digitalSignature, keyEncipherment\n", "{extendedKeyUsage}\n", "\n", "[ alt_names ]\n", "DNS.1 = {common_name}\n", "DNS.2 = {common_name}.{namespace}.svc.cluster.local # Use the namespace applicable for your cluster\n", "{alt_names}\n", "\"\"\"\n", "\n", "save_file(ssl_configuration_file, certificate)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Copy certificate configuration to `controller` `pod`" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import os\n", "\n", "cwd = os.getcwd()\n", "os.chdir(temp_dir) # Use chdir to workaround kubectl bug on Windows, which incorrectly processes 'c:\\' on kubectl cp cmd line \n", "\n", "run(f'kubectl exec {controller} -c controller -n {namespace} -- bash -c \"mkdir -p {test_cert_store_root}/{app_name}\"')\n", "\n", "run(f'kubectl cp {ssl_configuration_file} {controller}:{test_cert_store_root}/{app_name}/{ssl_configuration_file} -c controller -n {namespace}')\n", "\n", "os.chdir(cwd)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Generate certificate\n", "\n", "Use openssl req to generate a certificate in PKCS\\#10 format. See:\n", "\n", "- https://www.openssl.org/docs/man1.0.2/man1/req.html" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "cmd = f\"openssl req -config {test_cert_store_root}/{app_name}/service.openssl.cnf -newkey rsa:2048 -sha256 -nodes -days {days} -out {test_cert_store_root}/{app_name}/{prefix_keyfile_name}-signingrequest.csr -outform PEM -subj '/C={country_name}/ST={state_or_province_name}/L={locality_name}/O={organization_name}/OU={organizational_unit_name}/CN={common_name}'\"\n", "\n", "run(f'kubectl exec {controller} -n {namespace} -c controller -- bash -c \"{cmd}\"')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Clean up temporary directory for staging configuration files" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [ "hide_input" ] }, "outputs": [], "source": [ "# Delete the temporary directory used to hold configuration files\n", "\n", "import shutil\n", "\n", "shutil.rmtree(temp_dir)\n", "\n", "print(f'Temporary directory deleted: {temp_dir}')" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print('Notebook execution complete.')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Related\n", "-------\n", "\n", "- [CER021 - Create Knox\n", " certificate](../cert-management/cer021-create-knox-cert.ipynb)\n", "\n", "- [CER030 - Sign Management Proxy certificate with generated\n", " CA](../cert-management/cer030-sign-service-proxy-generated-cert.ipynb)\n", "\n", "- [CER040 - Install signed Management Proxy\n", " certificate](../cert-management/cer040-install-service-proxy-cert.ipynb)" ] } ], "nbformat": 4, "nbformat_minor": 5, "metadata": { "kernelspec": { "name": "python3", "display_name": "Python 3" }, "azdata": { "side_effects": true } } }