{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Administration: Validate all federated servers\n",
    "\n",
    "> * 🗄️ Administration\n",
    "* 🔔 Notifications\n",
    "* ✏️ Needs Configuration\n",
    "* 👟 Ready To Run!\n",
    "\n",
    "Enterprise installations most often contain federated servers to perform \n",
    "different roles within the infrastructure of the organization. Regularly\n",
    "validating the federated servers ensures proper functioning of technical \n",
    "components. The following notebook demonstrates how to validate servers within\n",
    "the Enterprise and send notifications if any servers fail to validate."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "To get started, let's import the necessary libraries and connect to our GIS."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "import pandas as pd\n",
    "import csv\n",
    "import smtplib\n",
    "import requests\n",
    "import logging\n",
    "log = logging.getLogger()\n",
    "\n",
    "from arcgis.gis import GIS\n",
    "\n",
    "gis = GIS(\"home\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Notifications\n",
    "\n",
    "An important part of this process is notifying the right personnel about servers that are not validating. We will achieve this by connecting to an external SMTP server and sending out emails, but you can write _any_ code that connects to _any_ external service to send out notifications. __View the 'Notifications' notebook in the examples gallery for more information__.\n",
    "\n",
    "__<span style=\"color:red;\">You MUST modify the below cell</span>__ to update `secret_csv_item_id`, `smtp_server_url`, `username`, and any other information needed to connect to your external smtp server. This includes making sure your _secrets.csv_ file item contains the `smtp_email_password` entry."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Then, make an empty .csv file with headers\n",
    "with open('./secrets.csv', \"w\") as f:\n",
    "    f.write(\"secret_key,secret_value\\n\")\n",
    "\n",
    "# Publish it as a private item only you and admins can see\n",
    "secret_csv_item = gis.content.add({'access':'private'}, './secrets.csv')\n",
    "secret_csv_item.protect()\n",
    "print(\"Secret CSV Item ID = '{}'\".format(secret_csv_item.id))\n",
    "secret_csv_item"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now that you have this blank CSV item created, copy and paste its item ID from above and make it a string constant for this notebook. Then, let's make a helper function that downloads the CSV and converts it to an easy-to-use dict."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "SECRET_CSV_ITEM_ID = '<item_id>'"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now, let's make a helper function to add a new secret. Each secret will have a secret_key to identify the secret, and the secret itself. Let's add an example 'external_service_api_key' secret."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def add_secret(secret_key, secret_value, gis=gis,\n",
    "               secret_csv_item_id = SECRET_CSV_ITEM_ID):\n",
    "    \"\"\"Appends a new secret to the secrets.csv item\"\"\"\n",
    "    item = gis.content.get(secret_csv_item_id)\n",
    "    with open(item.download(), 'a') as local_item_file:\n",
    "        local_item_file.write(\"{},{}\\n\".format(secret_key, secret_value))\n",
    "    return item.update({}, local_item_file.name)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "add_secret(\"smtp_email_password\", \"<password>\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**If you ever use this add_secret() function, make sure to delete the cell after calling it.** Leaving the function call in a notebook is as much of a security risk as saving your secret in a plain text string. Always double check your notebooks for strings, print outputs, and variables that can inadvertently expose secrets.\n",
    "\n",
    "The rest of the notebook will assume that you have added the appropriate secrets (passwords, API keys, private URLs, and so on) to the private CSV item you own."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Helper function for using your private secrets.csv file\n",
    "def get_secrets(gis=gis,\n",
    "                secret_csv_item_id = SECRET_CSV_ITEM_ID):\n",
    "    \"\"\"Returns a dict of { secret_key : secret_value } from the \n",
    "    secrets.csv item. See the 'Notifications' notebook in the \n",
    "    examples gallery for more information.\n",
    "    \"\"\"\n",
    "    try:\n",
    "        item = gis.content.get(secret_csv_item_id)\n",
    "        with open(item.download(), 'r') as local_item_file:\n",
    "            reader = csv.DictReader(local_item_file)\n",
    "            return { rows['secret_key'] : rows['secret_value'] \\\n",
    "                     for rows in reader }\n",
    "    except Exception:\n",
    "        return {}\n",
    "\n",
    "SECRETS = get_secrets()\n",
    "\n",
    "# Helper function to send out emails through an SMTP server\n",
    "def send_email_smtp(recipients, message,\n",
    "                    subject=\"Message from your Notebook\"):\n",
    "    \"\"\"Sends the `message` string to all of the emails in the \n",
    "    `recipients` list using the configured SMTP email server. \n",
    "    See the 'Notifications' notebook in the examples gallery\n",
    "    for more information.\n",
    "    \"\"\"\n",
    "    try:\n",
    "        # Set up server and credential variables\n",
    "        smtp_server_url = \"smtp.example.com\"\n",
    "        smtp_server_port = 587\n",
    "        sender = \"your_sender@example.com\"\n",
    "        username = \"<username>\"\n",
    "        password = SECRETS[\"smtp_email_password\"]\n",
    "\n",
    "        # Instantiate our server, configure the necessary security\n",
    "        server = smtplib.SMTP(smtp_server_url, smtp_server_port)\n",
    "        server.ehlo()\n",
    "        server.starttls() # Needed if TLS is required w/ SMTP server\n",
    "        server.login(username, password)\n",
    "    except Exception as e:\n",
    "        log.warning(\"Error setting up SMTP server, couldn't send \" +\n",
    "                    f\"message to {recipients}\")\n",
    "        raise e\n",
    "\n",
    "    # For each recipient, construct the message and attempt to send\n",
    "    did_succeed = True\n",
    "    for recipient in recipients:\n",
    "        try:\n",
    "            message_body = '\\r\\n'.join(['To: {}'.format(recipient),\n",
    "                                        'From: {}'.format(sender),\n",
    "                                        'Subject: {}'.format(subject),\n",
    "                                        '{}'.format(message)])\n",
    "            message_body = message_body.encode(\"utf-8\")\n",
    "            server.sendmail(sender, [recipient], message_body)\n",
    "            print(f\"SMTP server returned success for sending email \"\\\n",
    "                  f\"to {recipient}\")\n",
    "        except Exception as e:\n",
    "            log.warning(f\"Failed sending message to {recipient}\")\n",
    "            log.warning(e)\n",
    "            did_succeed = False\n",
    "    \n",
    "    # Cleanup and return\n",
    "    server.quit()\n",
    "    return did_succeed"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Retrieve Information on Servers"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "> **Note:** You must connect as an administrator to perform these steps.\n",
    "\n",
    "Next, let's use the `admin` property of the GIS to instantiate the [`PortalAdminManager`](https://esri.github.io/arcgis-python-api/apidoc/html/arcgis.gis.admin.html#portaladminmanager). We can access administrative options to help automate tasks such as checking the status of servers."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "portal_mgr = gis.admin"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The `federation` property on the portal admin object creates an instance of the [`Federation`](https://esri.github.io/arcgis-python-api/apidoc/html/arcgis.gis.admin.html#arcgis.gis.admin.Federation) resource.  This resource provides information and operations about the servers registered with the Enterprise.  In addition, it provides methods that allow you to automate federation and associated tasks.\n",
    "\n",
    "The [`servers`](https://esri.github.io/arcgis-python-api/apidoc/html/arcgis.gis.admin.html#arcgis.gis.admin.Federation.servers) property of the `federation` resource returns detailed output representing the GIS servers. We can create a list of the servers in the Enterprise:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[{'id': '8soO2ckl92RlN3Kf',\n",
       "  'name': 'ranalytics.example.com',\n",
       "  'url': 'https://ranalytics.example.com/arcgis',\n",
       "  'isHosted': False,\n",
       "  'adminUrl': 'https://ranalytics.example.com/arcgis',\n",
       "  'serverRole': 'FEDERATED_SERVER',\n",
       "  'serverFunction': 'RasterAnalytics',\n",
       "  'webgisServerTrustKey': ''},\n",
       " {'id': 'kPDQ7nw5mVnrt0Kj',\n",
       "  'name': 'nb_server.example.com',\n",
       "  'url': 'https://nb_server.example.com/turing',\n",
       "  'isHosted': False,\n",
       "  'adminUrl': 'https://nb_server.example.com/turing',\n",
       "  'serverRole': 'FEDERATED_SERVER',\n",
       "  'serverFunction': 'NotebookServer',\n",
       "  'webgisServerTrustKey': ''},\n",
       " {'id': '1YdjUwASdsbtXp5U',\n",
       "  'name': 'ganalytics.example.com',\n",
       "  'url': 'https://ganalytics.example.com/gax',\n",
       "  'isHosted': False,\n",
       "  'adminUrl': 'https://ganalytics.example.com/gax',\n",
       "  'serverRole': 'FEDERATED_SERVER',\n",
       "  'serverFunction': 'GeoAnalytics',\n",
       "  'webgisServerTrustKey': ''},\n",
       " {'id': '8kIlTw0RJRM3oBeX',\n",
       "  'name': 'dscience.example.com',\n",
       "  'url': 'https://dscience.example.com/server',\n",
       "  'isHosted': True,\n",
       "  'adminUrl': 'https://dscience.example.com/server',\n",
       "  'serverRole': 'HOSTING_SERVER',\n",
       "  'serverFunction': '',\n",
       "  'webgisServerTrustKey': ''}]"
      ]
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "fed_servers = portal_mgr.federation.servers['servers']\n",
    "fed_servers"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can quickly generate a list of each server along with its role and function within the Enterprise:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "https://ranalytics.example.com/arcgis                FEDERATED_SERVER    RasterAnalytics\n",
      "https://nb_server.example.com/turing                 FEDERATED_SERVER    NotebookServer\n",
      "https://ganalytics.example.com/gax                   FEDERATED_SERVER    GeoAnalytics\n",
      "https://dscience.example.com/server                  HOSTING_SERVER      \n"
     ]
    }
   ],
   "source": [
    "for fed_server in fed_servers:\n",
    "    print(f\"{fed_server['adminUrl']:<50}{fed_server['serverRole']:20}{fed_server['serverFunction']}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The [`validate_all()`](https://esri.github.io/arcgis-python-api/apidoc/html/arcgis.gis.admin.html#arcgis.gis.admin.Federation.validate_all) method returns detailed messaging about the status of all servers."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'status': 'success',\n",
       " 'serversStatus': [{'status': 'success with warnings',\n",
       "   'serverId': '8soO2hjr72RlN3Kf',\n",
       "   'messages': [\"ArcGIS Server administration URL 'https://ranalytics.example.com/arcgis' is accessible.\",\n",
       "    'The federated server does not have an ArcGIS Data Store registered as a managed database and cannot be set as a hosting server.',\n",
       "    'Validating RasterAnalytics server.',\n",
       "    \"ArcGIS Server 'https://ranalytics.example.com/arcgis' version matches with Portal for ArcGIS.\",\n",
       "    \"Verified that the server has a registered raster data store '/rasterStores/deeplearning'.\"]},\n",
       "  {'status': 'success with warnings',\n",
       "   'serverId': 'kFTT7nw5mVjxb0Kj',\n",
       "   'messages': [\"ArcGIS Server administration URL 'https://nb_server.example.com/turing' is accessible.\",\n",
       "    \"ArcGIS Server 'https://nb_server.example.com/turing' version is compatible with Portal for ArcGIS.\",\n",
       "    'The federated server does not have an ArcGIS Data Store registered as a managed database and cannot be set as a hosting server.']},\n",
       "  {'status': 'success with warnings',\n",
       "   'serverId': '1YtxUwASdsyyXp5M',\n",
       "   'messages': [\"ArcGIS Server administration URL 'https://ganalytics.example.com/gax' is accessible.\",\n",
       "    'The federated server does not have an ArcGIS Data Store registered as a managed database and cannot be set as a hosting server.',\n",
       "    'Validating GeoAnalytics server.',\n",
       "    \"ArcGIS Server 'https://ganalytics.example.com/gax' version matches with Portal for ArcGIS.\",\n",
       "    \"Verified that the hosting server has a registered spatiotemporal big data store '/nosqlDatabases/AGSDataStore_bigdata_bds_g1ppo8pn'.\"]},\n",
       "  {'status': 'success',\n",
       "   'serverId': '8kAjTw0RIEM3oBbX',\n",
       "   'messages': [\"ArcGIS Server administration URL 'https://dscience.example.com/server' is accessible.\",\n",
       "    'Validating hosting server.',\n",
       "    \"ArcGIS Server 'https://dscience.example.com/server' version matches with Portal for ArcGIS.\",\n",
       "    \"Verified that server has an ArcGIS Data Store registered as a managed database '/enterpriseDatabases/AGSDataStore_ds_fuwozw8p'.\",\n",
       "    \"The server managed database '/enterpriseDatabases/AGSDataStore_ds_fuwozw8p' validated successfully.\",\n",
       "    'Validated that the ArcGIS Server site is not in read-only mode.',\n",
       "    'Configured missing offlinePackaging helper services with Portal for ArcGIS.',\n",
       "    'Restarted OfflinePackaging service in the hosting ArcGIS Server.',\n",
       "    'Validated that the publishing tools service of ArcGIS Server site is started.']}]}"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "portal_mgr.federation.validate_all()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The [`validate()`](https://esri.github.io/arcgis-python-api/apidoc/html/arcgis.gis.admin.html#arcgis.gis.admin.Federation.validate) method can report on each server individually. We can use the unique server ids from the list we generated above to return any messages from a server that fails to validate or contains warnings about server status. \n",
    "\n",
    "We can then send an email with the messages from the server to inform the appropriate personnel to take action. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "def get_validate_status(server_id):\n",
    "    validate_resp = portal_mgr.federation.validate(server_id=server_id)\n",
    "    if not validate_resp['status'] == 'success':\n",
    "        return validate_resp"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can visualize the results (if we are running this interactively) to see what information is returned."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 32,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>messages</th>\n",
       "      <th>serverId</th>\n",
       "      <th>status</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>[ArcGIS Server administration URL 'https://ranalytics.example.com/arcgis' is accessible., The federated server does not have an ArcGIS Data Store registered as a managed database and cannot be set as...</td>\n",
       "      <td>8soO2hjr72RlN3Kf</td>\n",
       "      <td>success with warnings</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>[ArcGIS Server administration URL 'https://nb_server.example.com/turing' is accessible., ArcGIS Server 'https://nb_server.example.com/turing' version is compatible with Portal for ArcGIS., The federat...</td>\n",
       "      <td>kFTT7nw5mVjxb0Kj</td>\n",
       "      <td>success with warnings</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>[ArcGIS Server administration URL 'https://ganalytics.example.com/gax' is accessible., The federated server does not have an ArcGIS Data Store registered as a managed database and cannot be set as a ...</td>\n",
       "      <td>1YtxUwASdsyyXp5M</td>\n",
       "      <td>success with warnings</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                                                                                                                                                                                                  messages  \\\n",
       "0  [ArcGIS Server administration URL 'https://ranalytics.example.com/arcgis' is accessible., The federated server does not have an ArcGIS Data Store registered as a managed database and cannot be set as...   \n",
       "1  [ArcGIS Server administration URL 'https://nb_server.example.com/turing' is accessible., ArcGIS Server 'https://nb_server.example.com/turing' version is compatible with Portal for ArcGIS., The federat...   \n",
       "2  [ArcGIS Server administration URL 'https://ganalytics.example.com/gax' is accessible., The federated server does not have an ArcGIS Data Store registered as a managed database and cannot be set as a ...   \n",
       "\n",
       "           serverId                 status  \n",
       "0  8soO2hjr72RlN3Kf  success with warnings  \n",
       "1  kFTT7nw5mVjxb0Kj  success with warnings  \n",
       "2  1YtxUwASdsyyXp5M  success with warnings  "
      ]
     },
     "execution_count": 32,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "validation_list = []\n",
    "\n",
    "for ent_server in fed_servers:\n",
    "    validate_resp = get_validate_status(ent_server['id'])\n",
    "    if validate_resp:\n",
    "        validation_list.append(validate_resp)\n",
    "\n",
    "validate_df = pd.DataFrame(validation_list)\n",
    "validate_df"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Since we have results from the `get_validate_status` function, we can send an email to the appropriate personnel so the warnings and/or failures can be further investigated."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for ent_server in fed_servers:\n",
    "    validate_resp = get_validate_status(ent_server['id'])\n",
    "    if validate_resp:\n",
    "        send_email_smtp(recipients=['someone@example.com'],\n",
    "                        message='\\n'.join(validate_resp['messages']),\n",
    "                        subject=\"Message from Validation Notebook.\") "
   ]
  }
 ],
 "metadata": {
  "esriNotebookRuntime": {
   "notebookRuntimeName": "ArcGIS Notebook Python 3 Standard",
   "notebookRuntimeVersion": "10.7.1"
  },
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
