You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficcontrol.apache.org by ra...@apache.org on 2022/02/14 23:34:17 UTC

[trafficcontrol] branch master updated: Added CDNi ability to negotiate higher capacities (#6557)

This is an automated email from the ASF dual-hosted git repository.

rawlin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficcontrol.git


The following commit(s) were added to refs/heads/master by this push:
     new 661cc1e  Added CDNi ability to negotiate higher capacities (#6557)
661cc1e is described below

commit 661cc1e9f2a2fc41b4d296ff016b71c6c7fa5f0b
Author: mattjackson220 <33...@users.noreply.github.com>
AuthorDate: Mon Feb 14 16:33:58 2022 -0700

    Added CDNi ability to negotiate higher capacities (#6557)
    
    * Added CDNi ability to negotiate higher capacities
    
    * updated per comments
    
    * rewrote confusing code to find capability id from array of footprints
---
 CHANGELOG.md                                       |   1 +
 docs/source/admin/cdni.rst                         |  24 +
 docs/source/api/v4/oc_ci_configuration.rst         | 101 ++++
 docs/source/api/v4/oc_ci_configuration_host.rst    | 107 +++++
 .../v4/oc_ci_configuration_request_id_approved.rst |  60 +++
 .../traffic_router_golang/httpsrvr/httpsrvr.go     |   3 +-
 lib/go-rfc/http.go                                 |   1 +
 ...022020114365100_capacity_updates_queue.down.sql |  18 +
 .../2022020114365100_capacity_updates_queue.up.sql |  28 ++
 .../traffic_ops_golang/cdn/dnssecrefresh.go        |   3 +-
 traffic_ops/traffic_ops_golang/cdni/shared.go      | 512 +++++++++++++++++++--
 .../traffic_ops_golang/deliveryservice/acme.go     |   5 +-
 .../deliveryservice/autorenewcerts.go              |   3 +-
 traffic_ops/traffic_ops_golang/routing/routes.go   |   3 +
 14 files changed, 820 insertions(+), 49 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3634127..2e40b4f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
 ## [unreleased]
 ### Added
 - Added a new Traffic Ops endpoint to `GET` capacity and telemetry data for CDNi integration.
+- Added a Traffic Ops endpoints to `PUT` a requested configuration change for a full configuration or per host and an endpoint to approve or deny the request.
 
 ### Fixed
 - Update traffic_portal dependencies to mitigate `npm audit` issues.
diff --git a/docs/source/admin/cdni.rst b/docs/source/admin/cdni.rst
index 306ae47..b18db1e 100644
--- a/docs/source/admin/cdni.rst
+++ b/docs/source/admin/cdni.rst
@@ -34,3 +34,27 @@ For our use case, it is assumed that :abbr:`ATC (Apache Traffic Control)` is the
 .. seealso:: :ref:`to-api-oc-fci-advertisement`
 
 The advertisement response is unique for the :abbr:`uCDN (Upstream Content Delivery Network)` and contains the complete footprint and capabilities information structure the :abbr:`dCDN (Downstream Content Delivery Network)` wants to expose. This endpoint will return an array of generic :abbr:`FCI (Footprint and Capabilities Advertisement Interface)` base objects, including type, value, and footprint for each. Currently supported base object types are `FCI.Capacitiy` and `FCI.Telemetry` b [...]
+
+/OC/CI/configuration
+====================
+.. seealso:: :ref:`to-api-oc-fci-configuration`
+
+An endpoint that is used to push (``PUT``), fetch (``GET``), or delete (``DELETE``) the entire metadata set for a given :abbr:`uCDN (Upstream Content Delivery Network)` from a :abbr:`JWT (JSON Web Token)`. This puts the requested change into a queue to be reviewed later and returns an endpoint to view the asynchronous status updates.
+
+.. Note:: This is under construction. Currently only ``PUT`` is supported and in a very limited sense.
+
+/OC/CI/configuration/{{host}}
+=============================
+.. seealso:: :ref:`to-api-oc-fci-configuration-host`
+
+An endpoint that is used to push (``PUT``), fetch (``GET``), or delete (``DELETE``) the metadata set that is attached to host name for a given :abbr:`uCDN (Upstream Content Delivery Network)` from a :abbr:`JWT (JSON Web Token)`. This puts the requested change into a queue to be reviewed later and returns an endpoint to view the asynchronous status updates.
+
+.. Note:: This is under construction. Currently only ``PUT`` is supported and in a very limited sense.
+
+/OC/CI/configuration/request/{{id}}/{{approved}}
+================================================
+.. seealso:: :ref:`to-api-oc-fci-configuration-request-id-approved`
+
+This endpoint allows a user to approve or deny a queued update request from the previous endpoints. A denial will result in the removal from the queue and a ``FAILED`` status update. An approval will result in the changes being made to the configuration and a ``SUCCEEDED`` status update.
+
+.. Note:: This is under construction and only supports very limited metadata field and limited configuration updates.
diff --git a/docs/source/api/v4/oc_ci_configuration.rst b/docs/source/api/v4/oc_ci_configuration.rst
new file mode 100644
index 0000000..25d9dfc
--- /dev/null
+++ b/docs/source/api/v4/oc_ci_configuration.rst
@@ -0,0 +1,101 @@
+..
+..
+.. Licensed under the Apache License, Version 2.0 (the "License");
+.. you may not use this file except in compliance with the License.
+.. You may obtain a copy of the License at
+..
+..     http://www.apache.org/licenses/LICENSE-2.0
+..
+.. Unless required by applicable law or agreed to in writing, software
+.. distributed under the License is distributed on an "AS IS" BASIS,
+.. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+.. See the License for the specific language governing permissions and
+.. limitations under the License.
+..
+
+.. _to-api-oc-fci-configuration:
+
+***********************
+``OC/CI/configuration``
+***********************
+
+``PUT``
+=======
+Triggers an asynchronous task to update the configuration for the :abbr:`uCDN (Upstream Content Delivery Network)` by adding the request to a queue to be reviewed later. This returns a 202 Accepted status and an endpoint to be used for status updates.
+
+:Auth. Required: Yes
+:Roles Required: "admin" or "operations"
+:Permissions Required: CDNI:UPDATE
+:Response Type:  Object
+:Headers Required: "Authorization"
+
+Request Structure
+-----------------
+.. table:: Request Required Headers
+
+	+-----------------+------------------------------------------------------------------------------------------------------------------------------+
+	|    Name         | Description                                                                                                                  |
+	+=================+==============================================================================================================================+
+	|  Authorization  | A :abbr:`JWT (JSON Web Token)` provided by the :abbr:`dCDN (Downstream Content Delivery Network)` to identify the            |
+	|                 | :abbr:`uCDN (Upstream Content Delivery Network)`                                                                             |
+	+-----------------+------------------------------------------------------------------------------------------------------------------------------+
+
+:type: A string of the type of metadata to follow. See :rfc:`8006` for possible values. Only a selection of these are supported.
+:host: A string of the domain that the requested updates will change.
+:metadata: An array of generic metadata objects that conform to :rfc:`8006`.
+:generic-metadata-type: A string of the type of metadata to follow conforming to :rfc:`8006`.
+:generic-metadata-value: An array of generic metadata value objects conforming to :rfc:`8006` and :abbr:`SVA (Streaming Video Alliance)` specifications.
+
+.. code-block:: http
+	:caption: Example /OC/CI/configuration Request
+
+	PUT /api/4.0/oc/ci/configuration HTTP/1.1
+	Host: trafficops.infra.ciab.test
+	User-Agent: curl/7.47.0
+	Accept: */*
+	Cookie: mojolicious=...
+	Content-Length: 181
+	Content-Type: application/json
+
+	{
+		"type": "MI.HostMetadata",
+		"host": "example.com",
+		"metadata": [
+			{
+				"generic-metadata-type": "MI.RequestedCapacityLimits",
+				"generic-metadata-value": {
+					"requested-limits": [
+						{
+							"limit-type": "egress",
+							"limit-value": 20000,
+							"footprints": [
+								{
+									"footprint-type": "ipv4cidr",
+									"footprint-value": [
+										"127.0.0.1",
+										"127.0.0.2"
+									]
+								}
+							]
+						}
+					]
+				}
+			}
+		]
+	}
+
+Response Structure
+------------------
+
+.. code-block:: http
+	:caption: Response Example
+
+	HTTP/1.1 202 Accepted
+	Content-Type: application/json
+
+	{ "alerts": [
+		{
+			"text": "CDNi configuration update request received. Status updates can be found here: /api/4.0/async_status/1",
+			"level": "success"
+		}
+	]}
diff --git a/docs/source/api/v4/oc_ci_configuration_host.rst b/docs/source/api/v4/oc_ci_configuration_host.rst
new file mode 100644
index 0000000..a44ae961
--- /dev/null
+++ b/docs/source/api/v4/oc_ci_configuration_host.rst
@@ -0,0 +1,107 @@
+..
+..
+.. Licensed under the Apache License, Version 2.0 (the "License");
+.. you may not use this file except in compliance with the License.
+.. You may obtain a copy of the License at
+..
+..     http://www.apache.org/licenses/LICENSE-2.0
+..
+.. Unless required by applicable law or agreed to in writing, software
+.. distributed under the License is distributed on an "AS IS" BASIS,
+.. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+.. See the License for the specific language governing permissions and
+.. limitations under the License.
+..
+
+.. _to-api-oc-fci-configuration-host:
+
+********************************
+``OC/CI/configuration/{{host}}``
+********************************
+
+``PUT``
+=======
+Triggers an asynchronous task to update the configuration for the :abbr:`uCDN (Upstream Content Delivery Network)` and the specified host by adding the request to a queue to be reviewed later. This returns a 202 Accepted status and an endpoint to be used for status updates.
+
+:Auth. Required: Yes
+:Roles Required: "admin" or "operations"
+:Permissions Required: CDNI:UPDATE
+:Response Type:  Object
+:Headers Required: "Authorization"
+
+Request Structure
+-----------------
+.. table:: Request Required Headers
+
+	+-----------------+------------------------------------------------------------------------------------------------------------------------------+
+	|    Name         | Description                                                                                                                  |
+	+=================+==============================================================================================================================+
+	|  Authorization  | A :abbr:`JWT (JSON Web Token)` provided by the :abbr:`dCDN (Downstream Content Delivery Network)` to identify the            |
+	|                 | :abbr:`uCDN (Upstream Content Delivery Network)`                                                                             |
+	+-----------------+------------------------------------------------------------------------------------------------------------------------------+
+
+.. table:: Request Path Parameters
+
+	+-------+-----------------------------------------------------------------------------------+
+	| Name  |                 Description                                                       |
+	+=======+===================================================================================+
+	|  host | The text identifier for the host domain to be updated with the new configuration. |
+	+-------+-----------------------------------------------------------------------------------+
+
+:type: A string of the type of metadata to follow. See :rfc:`8006` for possible values. Only a selection of these are supported.
+:host-metadata: An array of generic metadata objects that conform to :rfc:`8006`.
+:generic-metadata-type: A string of the type of metadata to follow conforming to :rfc:`8006`.
+:generic-metadata-value: An array of generic metadata value objects conforming to :rfc:`8006` and :abbr:`SVA (Streaming Video Alliance)` specifications.
+
+.. code-block:: http
+	:caption: Example /OC/CI/configuration Request
+
+	PUT /api/4.0/oc/ci/configuration/example.com HTTP/1.1
+	Host: trafficops.infra.ciab.test
+	User-Agent: curl/7.47.0
+	Accept: */*
+	Cookie: mojolicious=...
+	Content-Length: 181
+	Content-Type: application/json
+
+	{
+		"type": "MI.HostMetadata",
+		"host-metadata": [
+			{
+				"generic-metadata-type": "MI.RequestedCapacityLimits",
+				"generic-metadata-value": {
+					"requested-limits": [
+						{
+							"limit-type": "egress",
+							"limit-value": 20000,
+							"footprints": [
+								{
+									"footprint-type": "ipv4cidr",
+									"footprint-value": [
+										"127.0.0.1",
+										"127.0.0.2"
+									]
+								}
+							]
+						}
+					]
+				}
+			}
+		]
+	}
+
+Response Structure
+------------------
+
+.. code-block:: http
+	:caption: Response Example
+
+	HTTP/1.1 202 Accepted
+	Content-Type: application/json
+
+	{ "alerts": [
+		{
+			"text": "CDNi configuration update request received. Status updates can be found here: /api/4.0/async_status/1",
+			"level": "success"
+		}
+	]}
diff --git a/docs/source/api/v4/oc_ci_configuration_request_id_approved.rst b/docs/source/api/v4/oc_ci_configuration_request_id_approved.rst
new file mode 100644
index 0000000..c709871
--- /dev/null
+++ b/docs/source/api/v4/oc_ci_configuration_request_id_approved.rst
@@ -0,0 +1,60 @@
+..
+..
+.. Licensed under the Apache License, Version 2.0 (the "License");
+.. you may not use this file except in compliance with the License.
+.. You may obtain a copy of the License at
+..
+..     http://www.apache.org/licenses/LICENSE-2.0
+..
+.. Unless required by applicable law or agreed to in writing, software
+.. distributed under the License is distributed on an "AS IS" BASIS,
+.. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+.. See the License for the specific language governing permissions and
+.. limitations under the License.
+..
+
+.. _to-api-oc-fci-configuration-request-id-approved:
+
+***************************************************
+``OC/CI/configuration/request/{{id}}/{{approved}}``
+***************************************************
+
+``PUT``
+=======
+Triggers an asynchronous task to update the configuration for the :abbr:`uCDN (Upstream Content Delivery Network)` and the specified host by adding the request to a queue to be reviewed later. This returns a 202 Accepted status and an endpoint to be used for status updates.
+
+:Auth. Required: Yes
+:Roles Required: "admin"
+:Permissions Required: CDNI-CAPACITY:ADMIN
+:Response Type:  Object
+
+Request Structure
+-----------------
+.. table:: Request Path Parameters
+
+	+-----------+----------------------------------------------------------------------------------------+
+	| Name      |                 Description                                                            |
+	+===========+========================================================================================+
+	|  id       | The integral identifier for the configuration update request to be approved or denied. |
+	+-----------+----------------------------------------------------------------------------------------+
+	|  approved | A boolean for whether to approve a configuration change request or not.                |
+	+-----------+----------------------------------------------------------------------------------------+
+
+Response Structure
+------------------
+
+.. code-block:: http
+	:caption: Response Example For Approved Change
+
+	HTTP/1.1 200 OK
+	Content-Type: application/json
+
+	{ "response": "Successfully updated configuration." }
+
+.. code-block:: http
+	:caption: Response Example For Denied Change
+
+	HTTP/1.1 200 OK
+	Content-Type: application/json
+
+	{ "response": "Successfully denied configuration update request." }
diff --git a/experimental/traffic_router_golang/httpsrvr/httpsrvr.go b/experimental/traffic_router_golang/httpsrvr/httpsrvr.go
index a535211..785ce2d 100644
--- a/experimental/traffic_router_golang/httpsrvr/httpsrvr.go
+++ b/experimental/traffic_router_golang/httpsrvr/httpsrvr.go
@@ -34,6 +34,7 @@ import (
 	"github.com/apache/trafficcontrol/experimental/traffic_router_golang/nextcache"
 
 	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/lib/go-rfc"
 	"github.com/apache/trafficcontrol/lib/go-tc"
 )
 
@@ -146,7 +147,7 @@ func getHandler(
 			newURL += "?" + r.URL.RawQuery
 		}
 
-		w.Header().Add("Location", newURL)
+		w.Header().Add(rfc.Location, newURL)
 		w.WriteHeader(http.StatusFound)
 	}
 }
diff --git a/lib/go-rfc/http.go b/lib/go-rfc/http.go
index 2ae1065..08fa281 100644
--- a/lib/go-rfc/http.go
+++ b/lib/go-rfc/http.go
@@ -41,6 +41,7 @@ const (
 	UserAgent          = "User-Agent"          // RFC7231§5.5.3
 	Vary               = "Vary"                // RFC7231§7.1.4
 	Age                = "Age"                 // RFC7234§5.1
+	Location           = "Location"            // RFC7231§7.1.2
 )
 
 // These are (some) valid values for content encoding and MIME types, for
diff --git a/traffic_ops/app/db/migrations/2022020114365100_capacity_updates_queue.down.sql b/traffic_ops/app/db/migrations/2022020114365100_capacity_updates_queue.down.sql
new file mode 100644
index 0000000..381453a
--- /dev/null
+++ b/traffic_ops/app/db/migrations/2022020114365100_capacity_updates_queue.down.sql
@@ -0,0 +1,18 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership.  The ASF
+ * licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+DROP TABLE IF EXISTS cdni_capability_updates;
diff --git a/traffic_ops/app/db/migrations/2022020114365100_capacity_updates_queue.up.sql b/traffic_ops/app/db/migrations/2022020114365100_capacity_updates_queue.up.sql
new file mode 100644
index 0000000..7ea094f
--- /dev/null
+++ b/traffic_ops/app/db/migrations/2022020114365100_capacity_updates_queue.up.sql
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership.  The ASF
+ * licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+CREATE TABLE IF NOT EXISTS cdni_capability_updates (
+                                                 id bigserial NOT NULL,
+                                                 request_type text NOT NULL,
+                                                 ucdn text NOT NULL,
+                                                 host text,
+                                                 data json NOT NULL,
+                                                 async_status_id bigint NOT NULL,
+                                                 last_updated timestamp with time zone DEFAULT now() NOT NULL,
+    CONSTRAINT pk_cdni_capability_updates PRIMARY KEY (id),
+    CONSTRAINT fk_cdni_capability_updates_async FOREIGN KEY (async_status_id) REFERENCES async_status(id) ON UPDATE CASCADE ON DELETE CASCADE
+);
diff --git a/traffic_ops/traffic_ops_golang/cdn/dnssecrefresh.go b/traffic_ops/traffic_ops_golang/cdn/dnssecrefresh.go
index 4db4e24..25c10ff 100644
--- a/traffic_ops/traffic_ops_golang/cdn/dnssecrefresh.go
+++ b/traffic_ops/traffic_ops_golang/cdn/dnssecrefresh.go
@@ -30,6 +30,7 @@ import (
 	"time"
 
 	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/lib/go-rfc"
 	"github.com/apache/trafficcontrol/lib/go-tc"
 	"github.com/apache/trafficcontrol/lib/go-util"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
@@ -63,7 +64,7 @@ func RefreshDNSSECKeysV4(w http.ResponseWriter, r *http.Request) {
 	if started {
 		message = "Starting DNSSEC key refresh in the background. This may take a few minutes. Status updates can be found here: " + api.CurrentAsyncEndpoint + strconv.Itoa(asyncStatusID)
 	}
-	w.Header().Add("Location", api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusID))
+	w.Header().Add(rfc.Location, api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusID))
 	api.WriteAlerts(w, r, http.StatusAccepted, tc.CreateAlerts(tc.SuccessLevel, message))
 }
 
diff --git a/traffic_ops/traffic_ops_golang/cdni/shared.go b/traffic_ops/traffic_ops_golang/cdni/shared.go
index 83dee5f..bcf3161 100644
--- a/traffic_ops/traffic_ops_golang/cdni/shared.go
+++ b/traffic_ops/traffic_ops_golang/cdni/shared.go
@@ -21,34 +21,50 @@ package cdni
 
 import (
 	"database/sql"
+	"encoding/json"
 	"errors"
 	"fmt"
-	"github.com/lib/pq"
 	"net/http"
+	"strconv"
+	"strings"
 	"time"
 
 	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/lib/go-rfc"
+	"github.com/apache/trafficcontrol/lib/go-tc"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
 
 	"github.com/dgrijalva/jwt-go"
+	"github.com/lib/pq"
 )
 
-const CapabilityQuery = `SELECT id, type, ucdn FROM cdni_capabilities WHERE type = $1 AND ucdn = $2`
-const AllFootprintQuery = `SELECT footprint_type, footprint_value::text[], capability_id FROM cdni_footprints`
+const (
+	CapabilityQuery   = `SELECT id, type, ucdn FROM cdni_capabilities WHERE type = $1 AND ucdn = $2`
+	AllFootprintQuery = `SELECT footprint_type, footprint_value::text[], capability_id FROM cdni_footprints`
 
-const totalLimitsQuery = `
+	totalLimitsQuery = `
 SELECT limit_type, maximum_hard, maximum_soft, ctl.telemetry_id, ctl.telemetry_metric, t.id, t.type, tm.name, ctl.capability_id 
 FROM cdni_total_limits AS ctl 
 LEFT JOIN cdni_telemetry as t ON telemetry_id = t.id 
 LEFT JOIN cdni_telemetry_metrics as tm ON telemetry_metric = tm.name`
 
-const hostLimitsQuery = `
+	hostLimitsQuery = `
 SELECT limit_type, maximum_hard, maximum_soft, chl.telemetry_id, chl.telemetry_metric, t.id, t.type, tm.name, host, chl.capability_id 
 FROM cdni_host_limits AS chl 
 LEFT JOIN cdni_telemetry as t ON telemetry_id = t.id 
 LEFT JOIN cdni_telemetry_metrics as tm ON telemetry_metric = tm.name 
 ORDER BY host DESC`
 
+	InsertCapabilityUpdateQuery                    = `INSERT INTO cdni_capability_updates (ucdn, data, async_status_id, request_type, host) VALUES ($1, $2, $3, $4, $5)`
+	SelectCapabilityUpdateQuery                    = `SELECT ucdn, data, async_status_id, request_type, host FROM cdni_capability_updates WHERE id = $1`
+	DeleteCapabilityUpdateQuery                    = `DELETE FROM cdni_capability_updates WHERE id = $1`
+	UpdateTotalLimitsByCapabilityAndLimitTypeQuery = `UPDATE cdni_total_limits SET maximum_hard = $1 WHERE capability_id = $2 AND limit_type = $3`
+	UpdateHostLimitsByCapabilityAndLimitTypeQuery  = `UPDATE cdni_host_limits SET maximum_hard = $1 WHERE capability_id = $2 AND limit_type = $3 AND host = $4`
+	hostQuery                                      = `SELECT count(*) FROM cdni_host_limits WHERE host = $1`
+
+	hostConfigLabel = "hostConfigUpdate"
+)
+
 func GetCapabilities(w http.ResponseWriter, r *http.Request) {
 	inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
 	if userErr != nil || sysErr != nil {
@@ -57,9 +73,50 @@ func GetCapabilities(w http.ResponseWriter, r *http.Request) {
 	}
 	defer inf.Close()
 
-	bearerToken := r.Header.Get("Authorization")
-	if bearerToken == "" {
-		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("bearer token header is required"), nil)
+	if inf.Config.Cdni == nil || inf.Config.Cdni.JwtDecodingSecret == "" || inf.Config.Cdni.DCdnId == "" {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("cdn.conf does not contain CDNi information"))
+		return
+	}
+
+	ucdn, err := checkBearerToken(r.Header.Get("Authorization"), inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+
+	capacities, err := getCapacities(inf, ucdn)
+	if err != nil {
+		api.HandleErr(w, r, nil, http.StatusInternalServerError, err, nil)
+		return
+	}
+
+	telemetries, err := getTelemetries(inf, ucdn)
+	if err != nil {
+		api.HandleErr(w, r, nil, http.StatusInternalServerError, err, nil)
+		return
+	}
+
+	fciCaps := Capabilities{}
+	capsList := make([]Capability, 0, len(capacities.Capabilities)+len(telemetries.Capabilities))
+	capsList = append(capsList, capacities.Capabilities...)
+	capsList = append(capsList, telemetries.Capabilities...)
+
+	fciCaps.Capabilities = capsList
+
+	api.WriteRespRaw(w, r, fciCaps)
+}
+
+func PutHostConfiguration(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"host"}, nil)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	host := inf.Params["host"]
+	if errCode, userErr, sysErr := validateHostExists(host, inf.Tx.Tx); userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
 		return
 	}
 
@@ -68,17 +125,365 @@ func GetCapabilities(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	ucdn, err := checkBearerToken(r.Header.Get("Authorization"), inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+
+	var genericHostRequest GenericHostMetadata
+	err = json.NewDecoder(r.Body).Decode(&genericHostRequest)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("decoding host json request: %w", err))
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+	asyncTx, err := db.Begin()
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async tx: %w", err))
+		return
+	}
+	logTx, err := db.Begin()
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting log tx: %w", err))
+		return
+	}
+	defer logTx.Commit()
+
+	asyncStatusId, errCode, userErr, sysErr := api.InsertAsyncStatus(asyncTx, "CDNi host configuration update request received.")
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	data := genericHostRequest.HostMetadata.Metadata
+
+	_, err = inf.Tx.Tx.Query(InsertCapabilityUpdateQuery, ucdn, data, asyncStatusId, hostConfigLabel, host)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("inserting capability update request into queue: %w", err))
+		return
+	}
+
+	msg := "CDNi configuration update request received. Status updates can be found here: " + api.CurrentAsyncEndpoint + strconv.Itoa(asyncStatusId)
+	api.CreateChangeLogRawTx(api.ApiChange, msg, inf.User, logTx)
+
+	var alerts tc.Alerts
+	alerts.AddAlert(tc.Alert{
+		Text:  msg,
+		Level: tc.SuccessLevel.String(),
+	})
+
+	w.Header().Add(rfc.Location, api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusId))
+	api.WriteAlerts(w, r, http.StatusAccepted, alerts)
+}
+
+func PutConfiguration(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil)
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	if inf.Config.Cdni == nil || inf.Config.Cdni.JwtDecodingSecret == "" || inf.Config.Cdni.DCdnId == "" {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("cdn.conf does not contain CDNi information"))
+		return
+	}
+
+	ucdn, err := checkBearerToken(r.Header.Get("Authorization"), inf)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
+		return
+	}
+
+	var genericRequest GenericRequestMetadata
+	err = json.NewDecoder(r.Body).Decode(&genericRequest)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("decoding json request: %w", err))
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+	asyncTx, err := db.Begin()
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async tx: %w", err))
+		return
+	}
+	logTx, err := db.Begin()
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting log tx: %w", err))
+		return
+	}
+	defer logTx.Commit()
+
+	asyncStatusId, errCode, userErr, sysErr := api.InsertAsyncStatus(asyncTx, "CDNi configuration update request received.")
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+
+	data := genericRequest.Metadata
+
+	_, err = inf.Tx.Tx.Query(InsertCapabilityUpdateQuery, ucdn, data, asyncStatusId, SupportedGenericMetadataType(genericRequest.Type), genericRequest.Host)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("inserting capability update request into queue: %w", err))
+		return
+	}
+
+	msg := "CDNi configuration update request received. Status updates can be found here: " + api.CurrentAsyncEndpoint + strconv.Itoa(asyncStatusId)
+	api.CreateChangeLogRawTx(api.ApiChange, msg, inf.User, logTx)
+	var alerts tc.Alerts
+	alerts.AddAlert(tc.Alert{
+		Text:  msg,
+		Level: tc.SuccessLevel.String(),
+	})
+
+	w.Header().Add(rfc.Location, api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusId))
+	api.WriteAlerts(w, r, http.StatusAccepted, alerts)
+}
+
+func PutConfigurationResponse(w http.ResponseWriter, r *http.Request) {
+	inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"approved", "id"}, []string{"id"})
+	if userErr != nil || sysErr != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr)
+		return
+	}
+	defer inf.Close()
+
+	reqId := inf.IntParams["id"]
+	approvedString := inf.Params["approved"]
+	approved, err := strconv.ParseBool(approvedString)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("approved parameter must be a boolean"), nil)
+		return
+	}
+
+	db, err := api.GetDB(r.Context())
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting async db: %w", err))
+		return
+	}
+
+	logTx, err := db.Begin()
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("getting log tx: %w", err))
+		return
+	}
+	defer logTx.Commit()
+
+	rows, err := inf.Tx.Tx.Query(SelectCapabilityUpdateQuery, reqId)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("querying for capability update request: %w", err))
+		return
+	}
+	defer log.Close(rows, "closing capabilities update query")
+	var ucdn string
+	var data json.RawMessage
+	var host string
+	var asyncId int
+	var requestType string
+	count := 0
+	for rows.Next() {
+		if err := rows.Scan(&ucdn, &data, &asyncId, &requestType, &host); err != nil {
+			api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("scanning db rows: %w", err))
+			return
+		}
+		count++
+	}
+	if count == 0 {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, fmt.Errorf("no configuration request for that id"), nil)
+		return
+	}
+
+	if !approved {
+		if asyncErr := api.UpdateAsyncStatus(db, api.AsyncFailed, "Requested configuration update has been denied.", asyncId, true); asyncErr != nil {
+			log.Errorf("updating async status for id %d: %s", asyncId, asyncErr.Error())
+		}
+		status, err := deleteCapabilityRequest(reqId, inf.Tx.Tx)
+		if err != nil {
+			api.HandleErr(w, r, inf.Tx.Tx, status, nil, fmt.Errorf("deleting configuration request from queue: %w", err))
+			return
+		}
+		msg := "Successfully denied configuration update request."
+		api.CreateChangeLogRawTx(api.ApiChange, msg, inf.User, inf.Tx.Tx)
+		api.WriteResp(w, r, msg)
+		return
+	}
+
+	var updatedDataList []GenericMetadata
+	if err = json.Unmarshal(data, &updatedDataList); err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("unmarshalling data for configuration update: %w", err))
+		return
+	}
+
+	var unsupportedTypes []string
+	for _, updatedData := range updatedDataList {
+		if !updatedData.Type.isValid() {
+			unsupportedTypes = append(unsupportedTypes, string(updatedData.Type))
+		}
+	}
+
+	if len(unsupportedTypes) != 0 {
+		api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, fmt.Errorf("unsupported generic metadata types found: %v", strings.Join(unsupportedTypes, ", ")), nil)
+		return
+	}
+
+	for _, updatedData := range updatedDataList {
+		switch updatedData.Type {
+		case MiRequestedCapacityLimits:
+			var capacityRequestedLimits CapacityRequestedLimits
+			if err = json.Unmarshal(updatedData.Value, &capacityRequestedLimits); err != nil {
+				api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("unmarshalling data for configuration update: %w", err))
+				return
+			}
+			for _, capLim := range capacityRequestedLimits.RequestedLimits {
+				capId, err := getCapabilityIdFromFootprints(capLim, ucdn, inf)
+				if err != nil {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("finding capability for given information: %w", err))
+					return
+				}
+
+				query := UpdateTotalLimitsByCapabilityAndLimitTypeQuery
+				queryParams := []interface{}{capLim.LimitValue, capId, capLim.LimitType}
+				if host != "" {
+					query = UpdateHostLimitsByCapabilityAndLimitTypeQuery
+					queryParams = []interface{}{capLim.LimitValue, capId, capLim.LimitType, host}
+				}
+
+				result, err := inf.Tx.Tx.Exec(query, queryParams...)
+				if err != nil {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("updating capacity: %w", err))
+					return
+				}
+
+				if rowsAffected, err := result.RowsAffected(); err != nil {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("updating capacity: getting rows affected: %w", err))
+					return
+				} else if rowsAffected < 1 {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, fmt.Errorf("no capacity found for update: host: %s, type: %s, limit: %v", host, updatedData.Type, capLim), nil)
+					return
+				} else if rowsAffected > 1 {
+					api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("capacity update affected too many rows: %d", rowsAffected))
+					return
+				}
+			}
+		}
+	}
+
+	if asyncErr := api.UpdateAsyncStatus(db, api.AsyncSucceeded, "Capacity requested update has been completed.", asyncId, true); asyncErr != nil {
+		log.Errorf("updating async status for id %v: %v", asyncId, asyncErr)
+	}
+	status, err := deleteCapabilityRequest(reqId, inf.Tx.Tx)
+	if err != nil {
+		api.HandleErr(w, r, inf.Tx.Tx, status, nil, fmt.Errorf("deleting capacity request from queue: %w", err))
+		return
+	}
+	msg := "Successfully updated configuration."
+	api.CreateChangeLogRawTx(api.ApiChange, msg, inf.User, logTx)
+	api.WriteResp(w, r, msg)
+}
+
+func getCapabilityIdFromFootprints(updatedData CapacityLimit, ucdn string, inf *api.APIInfo) (int, error) {
+	tableAbbr := ""
+	selectClause := ""
+	whereClause := ""
+	var queryParams []interface{}
+	paramCount := 1
+
+	for i, footprint := range updatedData.Footprints {
+		if i == 0 {
+			tableAbbr = "f"
+			selectClause = "SELECT " + tableAbbr + ".capability_id FROM cdni_footprints as " + tableAbbr
+			whereClause = " WHERE " + tableAbbr + ".ucdn = $" + strconv.Itoa(paramCount) + " AND " + tableAbbr + ".footprint_type = $" + strconv.Itoa(paramCount+1) + " AND " + tableAbbr + ".footprint_value = $" + strconv.Itoa(paramCount+2) + "::text[]"
+		} else {
+			oldTableAbbr := tableAbbr
+			tableAbbr = tableAbbr + "f"
+			selectClause = selectClause + " JOIN cdni_footprints as " + tableAbbr + " on " + tableAbbr + ".capability_id = " + oldTableAbbr + ".capability_id"
+			whereClause = whereClause + " AND " + tableAbbr + ".ucdn = $" + strconv.Itoa(paramCount) + " AND " + tableAbbr + ".footprint_type = $" + strconv.Itoa(paramCount+1) + " AND " + tableAbbr + ".footprint_value = $" + strconv.Itoa(paramCount+2) + "::text[]"
+		}
+		paramCount = paramCount + 3
+		queryParams = append(queryParams, ucdn)
+		queryParams = append(queryParams, footprint.FootprintType)
+		queryParams = append(queryParams, pq.Array(footprint.FootprintValue))
+	}
+
+	selectQuery := selectClause + whereClause + " AND (SELECT count(*) from cdni_footprints as c where c.capability_id = f.capability_id) = " + strconv.Itoa(len(updatedData.Footprints))
+	rows, err := inf.Tx.Tx.Query(selectQuery, queryParams...)
+	if err != nil {
+		return 0, fmt.Errorf("querying for capacity update request: %w", err)
+	}
+	defer log.Close(rows, "closing footprints query")
+	var capabilityIds []int
+	rowCount := 0
+	for rows.Next() {
+		var capabilityId int
+		if err := rows.Scan(&capabilityId); err != nil {
+			return 0, fmt.Errorf("scanning db rows: %w", err)
+		}
+		rowCount++
+		capabilityIds = append(capabilityIds, capabilityId)
+	}
+
+	if len(capabilityIds) == 0 {
+		return 0, fmt.Errorf("no capabilities found that match all footprints: %v", updatedData.Footprints)
+	}
+	if len(capabilityIds) > 1 {
+		return 0, fmt.Errorf("more than 1 capability found that match all footprints: %v", updatedData.Footprints)
+	}
+	return capabilityIds[0], nil
+}
+
+func deleteCapabilityRequest(reqId int, tx *sql.Tx) (int, error) {
+	result, err := tx.Exec(DeleteCapabilityUpdateQuery, reqId)
+	if err != nil {
+		return http.StatusInternalServerError, fmt.Errorf("deleting configuration update: %w", err)
+	}
+
+	if rowsAffected, err := result.RowsAffected(); err != nil {
+		return http.StatusInternalServerError, fmt.Errorf("deleting configuration update: getting rows affected: %w", err)
+	} else if rowsAffected < 1 {
+		return http.StatusNotFound, errors.New("no configuration update with that key found")
+	} else if rowsAffected > 1 {
+		return http.StatusInternalServerError, fmt.Errorf("delete affected too many rows: %d", rowsAffected)
+	}
+
+	return http.StatusOK, nil
+}
+
+func validateHostExists(host string, tx *sql.Tx) (int, error, error) {
+	count := 0
+	if err := tx.QueryRow(hostQuery, host).Scan(&count); err != nil {
+		return http.StatusInternalServerError, nil, fmt.Errorf("querying if host %s exists: %w", host, err)
+	}
+	if count == 0 {
+		return http.StatusBadRequest, fmt.Errorf("No data found for host: %s", host), nil
+	}
+	return http.StatusOK, nil, nil
+}
+
+func checkBearerToken(bearerToken string, inf *api.APIInfo) (string, error) {
+	if bearerToken == "" {
+		return "", errors.New("bearer token header is required")
+	}
+
 	claims := jwt.MapClaims{}
 	token, err := jwt.ParseWithClaims(bearerToken, claims, func(token *jwt.Token) (interface{}, error) {
 		return []byte(inf.Config.Cdni.JwtDecodingSecret), nil
 	})
 	if err != nil {
-		api.HandleErr(w, r, nil, http.StatusInternalServerError, fmt.Errorf("parsing claims: %w", err), nil)
-		return
+		return "", fmt.Errorf("parsing claims: %w", err)
 	}
 	if !token.Valid {
-		api.HandleErr(w, r, nil, http.StatusInternalServerError, errors.New("invalid token"), nil)
-		return
+		return "", errors.New("invalid token")
 	}
 
 	var expirationFloat float64
@@ -88,20 +493,17 @@ func GetCapabilities(w http.ResponseWriter, r *http.Request) {
 		switch key {
 		case "iss":
 			if _, ok := val.(string); !ok {
-				api.HandleErr(w, r, nil, http.StatusBadRequest, errors.New("invalid token - iss (Issuer) must be a string"), nil)
-				return
+				return "", errors.New("invalid token - iss (Issuer) must be a string")
 			}
 			ucdn = val.(string)
 		case "aud":
 			if _, ok := val.(string); !ok {
-				api.HandleErr(w, r, nil, http.StatusBadRequest, errors.New("invalid token - aud (Audience) must be a string"), nil)
-				return
+				return "", errors.New("invalid token - aud (Audience) must be a string")
 			}
 			dcdn = val.(string)
 		case "exp":
 			if _, ok := val.(float64); !ok {
-				api.HandleErr(w, r, nil, http.StatusBadRequest, errors.New("invalid token - exp (Expiration) must be a float64"), nil)
-				return
+				return "", errors.New("invalid token - exp (Expiration) must be a float64")
 			}
 			expirationFloat = val.(float64)
 		}
@@ -110,38 +512,16 @@ func GetCapabilities(w http.ResponseWriter, r *http.Request) {
 	expiration := int64(expirationFloat)
 
 	if expiration < time.Now().Unix() {
-		api.HandleErr(w, r, nil, http.StatusForbidden, errors.New("token is expired"), nil)
-		return
+		return "", errors.New("token is expired")
 	}
 	if dcdn != inf.Config.Cdni.DCdnId {
-		api.HandleErr(w, r, nil, http.StatusForbidden, errors.New("invalid token - incorrect dcdn"), nil)
-		return
+		return "", errors.New("invalid token - incorrect dcdn")
 	}
 	if ucdn == "" {
-		api.HandleErr(w, r, nil, http.StatusForbidden, errors.New("invalid token - empty ucdn field"), nil)
-		return
-	}
-
-	capacities, err := getCapacities(inf, ucdn)
-	if err != nil {
-		api.HandleErr(w, r, nil, http.StatusInternalServerError, err, nil)
-		return
-	}
-
-	telemetries, err := getTelemetries(inf, ucdn)
-	if err != nil {
-		api.HandleErr(w, r, nil, http.StatusInternalServerError, err, nil)
-		return
+		return "", errors.New("invalid token - empty ucdn field")
 	}
 
-	fciCaps := Capabilities{}
-	capsList := make([]Capability, 0, len(capacities.Capabilities)+len(telemetries.Capabilities))
-	capsList = append(capsList, capacities.Capabilities...)
-	capsList = append(capsList, telemetries.Capabilities...)
-
-	fciCaps.Capabilities = capsList
-
-	api.WriteRespRaw(w, r, fciCaps)
+	return ucdn, nil
 }
 
 func getFootprintMap(tx *sql.Tx) (map[int][]Footprint, error) {
@@ -318,6 +698,20 @@ const (
 	FciCapacityLimits                       = "FCI.CapacityLimits"
 )
 
+type SupportedGenericMetadataType string
+
+const (
+	MiRequestedCapacityLimits SupportedGenericMetadataType = "MI.RequestedCapacityLimits"
+)
+
+func (s SupportedGenericMetadataType) isValid() bool {
+	switch s {
+	case MiRequestedCapacityLimits:
+		return true
+	}
+	return false
+}
+
 type TelemetrySourceType string
 
 const (
@@ -332,3 +726,33 @@ const (
 	Asn                       = "asn"
 	CountryCode               = "countrycode"
 )
+
+type GenericHostMetadata struct {
+	Host         string           `json:"host"`
+	HostMetadata HostMetadataList `json:"host-metadata"`
+}
+
+type GenericRequestMetadata struct {
+	Type     string          `json:"type"`
+	Metadata json.RawMessage `json:"metadata"`
+	Host     string          `json:"host,omitempty"`
+}
+
+type HostMetadataList struct {
+	Metadata json.RawMessage `json:"metadata"`
+}
+
+type GenericMetadata struct {
+	Type  SupportedGenericMetadataType `json:"generic-metadata-type"`
+	Value json.RawMessage              `json:"generic-metadata-value"`
+}
+
+type CapacityRequestedLimits struct {
+	RequestedLimits []CapacityLimit `json:"requested-limits"`
+}
+
+type CapacityLimit struct {
+	LimitType  string      `json:"limit-type"`
+	LimitValue int64       `json:"limit-value"`
+	Footprints []Footprint `json:"footprints"`
+}
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/acme.go b/traffic_ops/traffic_ops_golang/deliveryservice/acme.go
index e89b8e6..4483248 100644
--- a/traffic_ops/traffic_ops_golang/deliveryservice/acme.go
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/acme.go
@@ -36,6 +36,7 @@ import (
 	"time"
 
 	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/lib/go-rfc"
 	"github.com/apache/trafficcontrol/lib/go-tc"
 	"github.com/apache/trafficcontrol/lib/go-util"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
@@ -228,7 +229,7 @@ func GenerateAcmeCertificates(w http.ResponseWriter, r *http.Request) {
 		Level: tc.SuccessLevel.String(),
 	})
 
-	w.Header().Add("Location", api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusId))
+	w.Header().Add(rfc.Location, api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusId))
 	api.WriteAlerts(w, r, http.StatusAccepted, alerts)
 }
 
@@ -321,7 +322,7 @@ func GenerateLetsEncryptCertificates(w http.ResponseWriter, r *http.Request) {
 		Level: tc.SuccessLevel.String(),
 	})
 
-	w.Header().Add("Location", api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusId))
+	w.Header().Add(rfc.Location, api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusId))
 	api.WriteAlerts(w, r, http.StatusAccepted, alerts)
 }
 
diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/autorenewcerts.go b/traffic_ops/traffic_ops_golang/deliveryservice/autorenewcerts.go
index 85aa3cf..ea6108a 100644
--- a/traffic_ops/traffic_ops_golang/deliveryservice/autorenewcerts.go
+++ b/traffic_ops/traffic_ops_golang/deliveryservice/autorenewcerts.go
@@ -28,6 +28,7 @@ import (
 	"time"
 
 	"github.com/apache/trafficcontrol/lib/go-log"
+	"github.com/apache/trafficcontrol/lib/go-rfc"
 	"github.com/apache/trafficcontrol/lib/go-tc"
 	"github.com/apache/trafficcontrol/lib/go-util"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
@@ -135,7 +136,7 @@ func renewCertificates(w http.ResponseWriter, r *http.Request, deprecated bool)
 		Level: tc.SuccessLevel.String(),
 	})
 
-	w.Header().Add("Location", api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusId))
+	w.Header().Add(rfc.Location, api.CurrentAsyncEndpoint+strconv.Itoa(asyncStatusId))
 	api.WriteAlerts(w, r, http.StatusAccepted, alerts)
 
 }
diff --git a/traffic_ops/traffic_ops_golang/routing/routes.go b/traffic_ops/traffic_ops_golang/routing/routes.go
index 8252faa..7915524 100644
--- a/traffic_ops/traffic_ops_golang/routing/routes.go
+++ b/traffic_ops/traffic_ops_golang/routing/routes.go
@@ -133,6 +133,9 @@ func Routes(d ServerData) ([]Route, http.Handler, error) {
 
 		// CDNI integration
 		{Version: api.Version{Major: 4, Minor: 0}, Method: http.MethodGet, Path: `OC/FCI/advertisement/?$`, Handler: cdni.GetCapabilities, RequiredPrivLevel: auth.PrivLevelReadOnly, RequiredPermissions: []string{"CDNI-CAPACITY:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 541357729077},
+		{Version: api.Version{Major: 4, Minor: 0}, Method: http.MethodPut, Path: `OC/CI/configuration/?$`, Handler: cdni.PutConfiguration, RequiredPrivLevel: auth.PrivLevelReadOnly, RequiredPermissions: []string{"CDNI-CAPACITY:UPDATE"}, Authenticated: Authenticated, Middlewares: nil, ID: 541357729078},
+		{Version: api.Version{Major: 4, Minor: 0}, Method: http.MethodPut, Path: `OC/CI/configuration/{host}?$`, Handler: cdni.PutHostConfiguration, RequiredPrivLevel: auth.PrivLevelReadOnly, RequiredPermissions: []string{"CDNI-CAPACITY:UPDATE"}, Authenticated: Authenticated, Middlewares: nil, ID: 541357729079},
+		{Version: api.Version{Major: 4, Minor: 0}, Method: http.MethodPut, Path: `OC/CI/configuration/request/{id}/{approved}?$`, Handler: cdni.PutConfigurationResponse, RequiredPrivLevel: auth.PrivLevelAdmin, RequiredPermissions: []string{"CDNI-CAPACITY:ADMIN"}, Authenticated: Authenticated, Middlewares: nil, ID: 541357729080},
 
 		// SSL Keys
 		{Version: api.Version{Major: 4, Minor: 0}, Method: http.MethodGet, Path: `sslkey_expirations/?$`, Handler: deliveryservice.GetSSlKeyExpirationInformation, RequiredPrivLevel: auth.PrivLevelAdmin, RequiredPermissions: []string{"SSL-KEY-EXPIRATION:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 41357729075},