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.
import keyring
keyring.set_password("ArcGIS Portal", "admin", "password")
# 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"
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 @