Select to view content in your preferred language

Listing Feature Classes Used By ArcGIS Server Services

7104
10
02-11-2022 11:40 AM
RogerDunnGIS
Frequent Contributor

Arriving at a new organization, I wasn't sure how our geodatabase feature classes and tables were used in ArcGIS Server.  If I want to shut down services locking down a feature class, which services should I shut down?  If I am replacing a feature class with a new one, which map documents and Pro projects should I modify and republish?  The script I created has served me, and I wanted to share it with you, after obfuscating our connection details.

As I am not allowed to attach .ipynp files to a post, there will be some copying and pasting involved.  I will describe exactly what you have to do to list out all of your services' dependencies.  You can then search the CSV output for the feature classes and tables you're wondering about, and see which services use them.

  1. Open Start > ArcGIS > Jupyter Notebook
  2. Browse to a folder where you would like to store a script about passwords, then click New > Python 3.  You'll have to change the name later.  Until then, it will be named Untitled.ipynp
  3. Copy and paste the following code into the cell:

 

import keyring
keyring.set_password("ArcGIS Portal", "admin", "password")

 

  1. Change "admin" to the username of your ArcGIS Server/Portal administrator
  2. Change "password" to the password of that account
  3. Run the cell by pressing Ctrl+Enter.  There will be no output and that's okay
  4. Close the browser tab representing the file
  5. In the Jupyter window listing your folders and files, check the box next to Untitled.ipynb (its icon will be a green book because it is running)
  6. Click the orange Shutdown button at the top of the list
  7. Check the box next to Untitled.ipynb again
  8. Click the Rename button at the top of the list
  9. Rename your file.  This file should be safeguarded by you.  You should NOT publish this file to a blog, code-sharing web site, or put it on a file server where lay users of your organization can find it, as it is pure text and contains a powerful password.  But you need to perform this step to run the code that will follow
  10. Once again in Jupyter, browser to a folder where you want the next script and click New > Python 3
  11. Copy and paste the code at the bottom of this post into the cell.  Feel absolutely free to split the code into logical blocks.  With your cursor at a desired spot in the code block, hit Ctrl+Shift+Minus.  This will allow you run blocks of code, one at a time, as you debug the code for your needs
  12. Read through the code, replacing text constants as needed.  For instance, replace variables like the adminURL with the actual URL to your ArcGIS Server Administrator Directory and tokenURL with the actual URL of your token generator.  [Finding these URLs is outside the scope of this post and can be found in the product documentation]
  13. With code divided into chunks as you please, run a cell using keyboard shortcuts like Ctrl+Enter (to run and keep the cell selected), Shift+Enter (to run the cell and select the next one down), or Alt+Enter (to run the cell and insert a new cell below).  Feel free to add print() statements to show the output of various commands
  14. Scripts are meant to be modified.  If a CSV output isn't what you want, modify the script to suit your needs!  Perhaps you want to pump the output to a real database table, a pandas DataFrame, or one huge JSON object, the choice is yours!  The tricky part is the many-to-many nature of it all (many services can use a feature class, and many feature classes can be part of one service)

 

# The requests library is necessary for making http(s) calls to REST services
# The keyring library is necessary for obtaining the password for your ArcGIS Server username.  See documentation below.
# The csv library is necessary because the final output for this script is a CSV file.
# The json library is necessary for pretty printing JSON output.  Humans can more easily see property names, values,
# and parent/child relationships
import requests, keyring, csv, json
# On any one system, Python programmers can import the keyring module and call set_password with a type, user, and password.
# I have done that on my system, and assigned the password via keyring so that it does not appear in this script.
# I access that password using the type and user that was originally used in the call to set_password.
keyringType = "ArcGIS Portal"
# The user name of an ArcGIS Server administrator, used in keyring to store its password
administratorUser = "admin"
# How long the ArcGIS Server token will last
tokenExpiry = 30 # In minutes
# The kind of client looking for a token
tokenClient = "referrer"
# When in Jupyter Notebook, this is the base URL host in the address bar, and is used in obtaining a token
tokenReferer = "localhost:8888"
# The URL to generate a token in ArcGIS Server, or, if using Federated servers, ArcGIS Portal.
tokenURL = "https://maps.yourentity.com/portal/sharing/rest/generateToken"
# The ArcGIS Server Admin REST URL, tied to a web adaptor.  Sometimes this has a :6443 in it.
adminURL = "https://maps.yourentity.com/server/admin"
servicesURL = adminURL + "/services"
# The name of the output CSV file for this script
outputCSVName = "ArcGISServerDump.csv"
# The format of the output CSV file.  See https://docs.python.org/3/library/csv.html#dialects-and-formatting-parameters for more info.
csvDialect = "excel"
# Do not change this constant.  It is appended to URLs to obtain the service manifest, which contains a lot of important
# metadata about the service.
manifestSuffix = "iteminfo/manifest/manifest.json"
# Do not change this constant.  These are URL parameters for obtaining what this script expects
servicesParams = {"detail": "false", "f": "json"}
# Do not change this constant.  This is a URL parameter for obtaining what this script expects
jsonParams = {"f": "json"}
# Obtain the password stored in keyring
password = keyring.get_password(keyringType, administratorUser)
# Do not change this variable.  These are URL parameters sent in the POST to obtain a token
tokenParams = {"username": administratorUser, "password": password, "expiration": tokenExpiry, "client": tokenClient, "referer": tokenReferer, "f": "json"}
# Obtain an ArcGIS Server token
r = requests.post(tokenURL, data=tokenParams)
# Show the http status code
print("Http Status Code Returned:", r.status_code)
# Transform the result to JSON
result = r.json()
# Get the token
token = result["token"]
# Show the token, so it can be copied and pasted, if needed, for debugging, or using in a browser
print(token)
# Do not change this variable.  These are http headers for obtaining responses using authentication and authorization
# via ArcGIS Server and/or Portal
servicesHeader = {"Content-Type": "application/json", "X-Esri-Authorization": "Bearer {0}".format(token)}
# Get all of the services in an http response
servicesResponse = requests.get(servicesURL, params=servicesParams, headers=servicesHeader)
# Transform the response to JSON
servicesJson = servicesResponse.json()
# Get a list of all the folders
gisFolders = servicesJson["folders"]
# Since the root folder is never listed, add it at the front of a new list
realFolders = ["Root"] + gisFolders
# Optionally, print the list of ArcGIS Server folders
print(realFolders)
# Create/overwrite the output CSV.  If the CSV is open, especially in Excel, then this part of the script will fail
with open(outputCSVName, "w", newline='') as outFile:
    # Create a CSV Dictionary Writer object
    outWriter = csv.DictWriter(outFile, quoting=csv.QUOTE_MINIMAL, dialect="excel"
            , fieldnames=["Folder", "Service", "Type", "Manifest", "Source#", "Source", "Client", "Database#", "Server", "Database", "Version", "Username", "Layer#", "Table"])
    # Write the names of the fields as the first line in the file
    outWriter.writeheader()
    # For every ArcGIS Server folder...
    for folder in realFolders:
        # Create (or replace) an empty output dictionary (outionary)
        outionary = {}
        # Construct the service URL differently for the root folder than for other folders
        outionary["Folder"] = folder
        if folder == "Root":
            servicesListURL = servicesURL
        else:
            servicesListURL = "{0}/{1}".format(servicesURL, folder)
        # Get an http response of the services in the current folder
        servicesResponse = requests.get(servicesListURL, params=servicesParams, headers=servicesHeader)
        # Transform the response to JSON
        servicesJson = servicesResponse.json()
        # Get a JSON list of the services
        gisServices = servicesJson["services"]
        # For every service in the folder...
        for gisService in gisServices:
            # Wipe out dictionary values assigned in other parts of this loop.  I don't use the del command because the dictionary
            # might not have these keys yet.
            outionary["Source#"] = outionary["Source"] = outionary["Client"] = outionary["Database#"] = outionary["Layer#"] = outionary["Table"] = ""
            # Assign the Service and Type in the dictionary, which map to the CSV DictWriter fields above
            outionary["Service"] = gisService["serviceName"]
            outionary["Type"] = gisService["type"]
            # This script is only interested in MapServers and ImageServers.  You can change this if statement to query other types of services
            if gisService["type"] == "MapServer" or gisService["type"] == "ImageServer":
                # Construct the URL to the manifest file
                # Construct the URL to the manifest file
                if folder == "Root":
                    manifestURL = "{0}/services/{1}.{2}/{3}".format(adminURL, gisService["serviceName"], gisService["type"], manifestSuffix)
                else:
                    manifestURL = "{0}/services/{1}/{2}.{3}/{4}".format(adminURL, folder, gisService["serviceName"], gisService["type"], manifestSuffix)
                # Assign the Manifest value in the dicionary, which will be written out later
                outionary["Manifest"] = manifestURL
                # Obtain an http response of the manifest file
                manifestResponse = requests.get(manifestURL, params=jsonParams, headers=servicesHeader)
                # Http status 200 is a good thing
                if manifestResponse.status_code == 200:
                    # Transform the response to JSON
                    manifestJson = manifestResponse.json()
                    # If a property called databases is in the response...
                    if "databases" in manifestJson:
                        # Start counting
                        iBase = 0
                        # For every database listed in databases...
                        for database in manifestJson["databases"]:
                            # Assign the Database# index number in the dictionary
                            outionary["Database#"] = iBase
                            outWriter.writerow(outionary)
                            iBase = iBase + 1
                            # If the database has an onServerConnectionString value, read it.  Other possible, meaningful values are
                            # onClientConnectionString.
                            if "onServerConnectionString" in database:
                                # The connection string looks like most other database connection strings you've seen.
                                connectionString = database["onServerConnectionString"]
                                # The connection string is a list of name=value pairs, separated by semicolons, at least in my experience,
                                # when dealing with geodatabases.  But onServerConnectionString can also be a file path.
                                # Create an empty dictionary for connection parameters
                                connectDict = {}
                                if ";" in connectionString:
                                    connectStringParams = connectionString.split(";")
                                    # For each connection parameter in the connection string...
                                    for connectParam in connectStringParams:
                                        # Wipe out existing values in the output dictionary, from Server to Username
                                        outionary["Server"] = outionary["Database"] = outionary["Version"] = outionary["Username"] = ""
                                        # Split this connection parameter into name=value
                                        nameValue = connectParam.split("=")
                                        # For example, 'USER=sde' will result in a dictionary entry "USER": "sde" in connectDict.
                                        connectDict[nameValue[0]] = nameValue[1]
                                        # Search for specific names in the connection parameters and write them to the output dictionary.
                                        # Your database connection strings may have different values that those shown here.  If this is
                                        # the case, modify these if and then statements to reflect name=value pairs in those.
                                        if "SERVER" in connectDict:
                                            outionary["Server"] = connectDict["SERVER"]
                                        if "DATABASE" in connectDict:
                                            outionary["Database"] = connectDict["DATABASE"]
                                        if "VERSION" in connectDict:
                                            outionary["Version"] = connectDict["VERSION"]
                                        if "USER" in connectDict:
                                            outionary["Username"] = connectDict["USER"]
                                else:
                                    # See above for similar logic
                                    nameValue = connectionString.split("=")
                                    connectDict[nameValue[0]] = nameValue[1]
                                    if "DATABASE" in connectDict:
                                        outionary["Database"] = connectDict["DATABASE"]
                            # Start counting layers and tables that come from that database
                            iLayer = 0
                            # For each dataset listed...
                            for dataset in database["datasets"]:
                                # Add the layer's index to the output dictionary
                                outionary["Layer#"] = iLayer
                                iLayer = iLayer + 1
                                # Add the layer's feature class or table name to the output dictionary
                                outionary["Table"] = dataset["onServerName"]
                                # Write the row to the output.  This will result in multiple rows containing similar information,
                                # but the redundancy will be helpful
                                outWriter.writerow(outionary)
                    # Now that we're done with the databases node, wipe out database properties in the output dictionary
                    outionary["Server"] = outionary["Database"] = outionary["Version"] = outionary["Username"] = outionary["Layer#"] = outionary["Table"] = ""
                    # If there is a resources section in the response...
                    if "resources" in manifestJson:
                        # Start counting resources
                        iResource = 0
                        # For each resource list...
                        for resource in manifestJson["resources"]:
                            # Add the resource's index to the output dictionary
                            outionary["Source#"] = iResource
                            iResource = iResource + 1
                            # If an onPremisePath is listed...
                            if "onPremisePath" in resource:
                                # Write the path to the output dictionary, which is typically the name of the ArcMap document or ArcGIS Pro project
                                # which published the service.
                                outionary["Source"] = resource["onPremisePath"]
                            if "clientName" in resource:
                                outionary["Client"] = resource["clientName"]
                            outWriter.writerow(outionary)
​outfile.close()

 

 

The resulting file will likely be stored in the same place as the script.  You control the name of the file with the constant outputCSVName in the script.

I hope this post has helped you, even if it has only inspired you to write your own.  Feel free to follow me, as I tend to post reusable scripts that help me do my job, and they may help you use yours.  And please ask questions if something fails; if I don't know the answer I'll say as much.

"Roj"

10 Replies
ScottM
by
New Contributor

@BradleyCartledge 

I know it's been a while but, wanted to say thank you! This PowerShell script was very helpful. I had to add a .Replace() method to the $service.folderName part of building the $ServiceItem object to account for services in our root folder but, after that it worked great!

# Connect to arcgis server admin
$adminURL = "https://<serverurl>/admin"

# To acquire portal token, open https://<portalurl>/sharing/rest/generateToken
# and enter ‘https://<serverurl>/admin’ for the ‘Webapp URL’ parameter.

# Set token variable to value
$Token = "<token>"

# Adminstrative path on arcgis server pointing to location where services are saved. 
$administrativePath = '\\<server>\<arcgis drive>$\arcgis\arcgisserver\directories\arcgissystem\arcgisinput'

# Get all services and folders from server
$rootServices = Invoke-RestMethod -uri "$adminURL/services?f=json&token=$Token" -Method Get

# Get root services
$services = @()
foreach($service in $rootServices.services){
    $services += $service   
}
 
# Loop through folders   
foreach($folder in $rootServices.folders){
    $folderServices = Invoke-RestMethod -uri "$adminURL/services/$($folder)?f=json&token=$Token" -Method Get
    foreach($service in $folderServices.services){
        $services += $service
    }
}

# Only grab MapServer services
$Services = $Services | Where-Object {$_.Type -eq "MapServer"}

# Get mapx files corresponding to mapserver service
$MapServices = @()
foreach($service in $services){
    $Mapx = Get-ChildItem -Path "$administrativePath\$($service.folderName)\$($service.serviceName).$($service.type)" -Recurse -File -Filter "*.mapx"
    $ServiceItem = [PSCustomObject]@{
        Folder = $service.folderName.Replace('\','')
        Name = $service.serviceName
        Type = $service.type
        MapX = $Mapx.FullName
    }
    $MapServices += $ServiceItem
}

# Filter MapServer Services with files
$MapServices = $MapServices | Where-Object {$_.MapX -ne $null}

# Read mapx json to get layers and dataset connection details
$layers = @()
foreach($service in $MapServices){
    $Map = Get-Content $Service.mapx | ConvertFrom-json
    foreach($layer in $Map.layerDefinitions){
        $LayerItem = [PSCustomObject]@{
            Folder = $service.folder
            Name = $service.Name
            Type = $service.type
            MapX = $service.mapx
            LayerName = $layer.name
            DBConnection = $layer.featureTable.dataConnection.workspaceConnectionString
            Dataset = $layer.featureTable.dataConnection.dataset
        }
        $layers += $layerItem
    }
}

$Layers | Export-CSV '<csv path>' -NoTypeInformation 

0 Kudos