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 d417c69  Added CDNi Capacity and Telemetry (#6524)
d417c69 is described below

commit d417c69e3aa4d9d7637b6dbcc7cc8132427952ba
Author: mattjackson220 <[email protected]>
AuthorDate: Thu Feb 10 16:12:42 2022 -0700

    Added CDNi Capacity and Telemetry (#6524)
    
    * Added CDNi Capacity and Telemetry
    
    * updated changelog
    
    * added cdni doc to toc
    
    * added config check
    
    * updated per comments
    
    * updated per comments
    
    * got the one i missed
---
 CHANGELOG.md                                       |   3 +
 docs/source/admin/cdni.rst                         |  36 +++
 docs/source/admin/index.rst                        |   1 +
 docs/source/admin/traffic_ops.rst                  |   7 +
 docs/source/api/v4/oc_fci_advertisement.rst        | 132 ++++++++
 traffic_ops/app/conf/cdn.conf                      |   4 +
 ...011112591400_added_cdni_capacity_table.down.sql |  23 ++
 ...22011112591400_added_cdni_capacity_table.up.sql |  82 +++++
 traffic_ops/traffic_ops_golang/cdni/capacity.go    | 156 ++++++++++
 traffic_ops/traffic_ops_golang/cdni/shared.go      | 334 +++++++++++++++++++++
 traffic_ops/traffic_ops_golang/cdni/telemetry.go   |  94 ++++++
 traffic_ops/traffic_ops_golang/config/config.go    |   6 +
 traffic_ops/traffic_ops_golang/routing/routes.go   |   4 +
 13 files changed, 882 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1dd790a..0d5c3ca 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,9 @@ All notable changes to this project will be documented in this 
file.
 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.
+
 ### Fixed
 - Update traffic_portal dependencies to mitigate `npm audit` issues.
 - Fixed a cdn-in-a-box build issue when using `RHEL_VERSION=7`
diff --git a/docs/source/admin/cdni.rst b/docs/source/admin/cdni.rst
new file mode 100644
index 0000000..306ae47
--- /dev/null
+++ b/docs/source/admin/cdni.rst
@@ -0,0 +1,36 @@
+..
+..
+.. 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.
+..
+
+.. _cdni_admin:
+
+****************************
+CDNi Administration
+****************************
+
+:abbr:`CDNi (Content Delivery Network Interconnect)` specifications define the 
standards for interoperability within the :abbr:`CDN (Content Delivery 
Network)` and open caching ecosystems set forth by the :abbr:`IETF (Internet 
Engineering Task Force)`. This integration utilizes the :abbr:`APIs 
(Application Programming Interfaces)` defined by the :abbr:`SVA (Streaming 
Video Alliance)`.
+
+.. seealso:: For complete details on CDNi and the related API specifications 
see :rfc:`8006`, :rfc:`8007`, :rfc:`8008`, and the :abbr:`SVA (Streaming Video 
Alliance)` documents titled `Footprint and Capabilities Interface: Open Caching 
API`, `Open Caching API Implementation Guidelines`, `Configuration Interface: 
Part 1 Specification - Overview & Architecture`, `Configuration Interface: Part 
2 Specification – CDNi Metadata Model Extensions`, and `Configuration 
Interface: Part 3 Specificat [...]
+
+In short, these documents describe the :abbr:`CDNi (Content Delivery Network 
Interconnect)` metadata interface that enables interconnected :abbr:`CDNs 
(Content Delivery Networks)` to exchange content distribution metadata to 
enable content acquisition and delivery. These define the interfaces through 
which a :abbr:`uCDN (Upstream Content Delivery Network)` and a :abbr:`dCDN 
(Downstream Content Delivery Network)` can communicate configuration and 
capacity information.
+
+For our use case, it is assumed that :abbr:`ATC (Apache Traffic Control)` is 
the :abbr:`dCDN (Downstream Content Delivery Network)`.
+
+       ..  Note:: This is currently under construction and will be for a 
while. This document will be updated as new features are supported.
+
+/OC/FCI/advertisement
+=====================
+.. 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 [...]
diff --git a/docs/source/admin/index.rst b/docs/source/admin/index.rst
index a039b1b..bc9fde5 100644
--- a/docs/source/admin/index.rst
+++ b/docs/source/admin/index.rst
@@ -52,3 +52,4 @@ Once everything is installed, you will need to configure the 
servers to talk to
        t3c/index.rst
        traffic_vault.rst
        quick_howto/index.rst
+       cdni.rst
diff --git a/docs/source/admin/traffic_ops.rst 
b/docs/source/admin/traffic_ops.rst
index 9fe862c..c95ad83 100644
--- a/docs/source/admin/traffic_ops.rst
+++ b/docs/source/admin/traffic_ops.rst
@@ -494,6 +494,13 @@ This file deals with the configuration parameters of 
running Traffic Ops itself.
 
        .. versionadded:: 6.1
 
+:cdni: This is an optional section of configurations for :abbr:`CDNi (Content 
Delivery Network Interconnect)` operations.
+
+       .. versionadded:: 6.2
+
+       :dcdn_id: A string representing this :abbr:`CDN (Content Delivery 
Network)` to be used in the :abbr:`JWT (JSON Web Token)` and subsequently in 
:abbr:`CDNi (Content Delivery Network Interconnect)` operations.
+       :jwt_decoding_secret: A string used to decode the :abbr:`JWT (JSON Web 
Token)` to get information for :abbr:`CDNi (Content Delivery Network 
Interconnect)` operations.
+
 
 Example cdn.conf
 ''''''''''''''''
diff --git a/docs/source/api/v4/oc_fci_advertisement.rst 
b/docs/source/api/v4/oc_fci_advertisement.rst
new file mode 100644
index 0000000..319c7a9
--- /dev/null
+++ b/docs/source/api/v4/oc_fci_advertisement.rst
@@ -0,0 +1,132 @@
+..
+..
+.. 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-advertisement:
+
+************************
+``OC/FCI/advertisement``
+************************
+
+``GET``
+=======
+Returns the complete footprint and capabilities information structure the 
:abbr:`dCDN (Downstream Content Delivery Network)` wants to expose to a given 
:abbr:`uCDN (Upstream Content Delivery Network)`.
+
+:Auth. Required: No
+:Roles Required: "admin" or "operations"
+:Permissions Required: CDNI:READ
+:Response Type:  Array
+: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)`. 
This token must include the following claims:                              |
+       |                 |                                                     
                                                                         |
+       |                 | - ``iss`` Issuer claim as a string key for the 
:abbr:`uCDN (Upstream Content Delivery Network)`                              |
+       |                 | - ``aud`` Audience claim as a string key for the 
:abbr:`dCDN (Downstream Content Delivery Network)`                          |
+       |                 | - ``exp`` Expiration claim as the expiration date 
as a Unix epoch timestamp (in seconds)                                     |
+       
+-----------------+------------------------------------------------------------------------------------------------------------------------------+
+
+Response Structure
+------------------
+:capabilities:     An array of generic :abbr:`FCI (Footprint and Capabilities 
Advertisement Interface)` base objects.
+:capability-type:  A string of the type of base object.
+:capability-value: An array of the value for the base object.
+:footprints:       An array of footprints impacted by this generic base object.
+
+       .. note:: These are meant to be generic and therefore there is not much 
information in these documents. For further information please see :rfc:`8006`, 
:rfc:`8007`, :rfc:`8008`, and the :abbr:`SVA (Streaming Video Alliance)` 
documents titled `Footprint and Capabilities Interface: Open Caching API`, 
`Open Caching API Implementation Guidelines`, `Configuration Interface: Part 1 
Specification - Overview & Architecture`, `Configuration Interface: Part 2 
Specification – CDNi Metadata Model E [...]
+
+.. code-block:: json
+       :caption: Example /OC/FCI/advertisement Response
+
+       {
+               "capabilities": [
+                       {
+                               "capability-type": "FCI.CapacityLimits",
+                               "capability-value": [
+                                       {
+                                               "total-limits": [
+                                                       {
+                                                               "limit-type": 
"egress",
+                                                               "maximum-hard": 
5000,
+                                                               "maximum-soft": 
2500,
+                                                               
"telemetry-source": {
+                                                                       "id": 
"capacity_metrics",
+                                                                       
"metric": "capacity"
+                                                               }
+                                                       }
+                                               ],
+                                               "host-limits": [
+                                                       {
+                                                               "host": 
"example.com",
+                                                               "limits": [
+                                                                       {
+                                                                               
"limit-type": "requests",
+                                                                               
"maximum-hard": 100,
+                                                                               
"maximum-soft": 50,
+                                                                               
"telemetry-source": {
+                                                                               
        "id": "request_metrics",
+                                                                               
        "metric": "requests"
+                                                                               
}
+                                                                       }
+                                                               ]
+                                                       }
+                                               ]
+                                       }
+                               ],
+                               "footprints": [
+                                       {
+                                               "footprint-type": "countrycode",
+                                               "footprint-value": [
+                                                       "us"
+                                               ]
+                                       }
+                               ]
+                       },
+                       {
+                               "capability-type": "FCI.Telemetry",
+                               "capability-value": {
+                                       "sources": [
+                                               {
+                                                       "id": 
"capacity_metrics",
+                                                       "type": "generic",
+                                                       "metrics": [
+                                                               {
+                                                                       "name": 
"capacity",
+                                                                       
"time-granularity": 0,
+                                                                       
"data-percentile": 50,
+                                                                       
"latency": 0
+                                                               }
+                                                       ]
+                                               }
+                                       ]
+                               },
+                               "footprints": [
+                                       {
+                                               "footprint-type": "countrycode",
+                                               "footprint-value": [
+                                                       "us"
+                                               ]
+                                       }
+                               ]
+                       }
+               ]
+       }
+
diff --git a/traffic_ops/app/conf/cdn.conf b/traffic_ops/app/conf/cdn.conf
index 6a00e20..08fc3e3 100644
--- a/traffic_ops/app/conf/cdn.conf
+++ b/traffic_ops/app/conf/cdn.conf
@@ -98,5 +98,9 @@
         "organization" : "",
         "country" : "",
         "state" : ""
+    },
+    "cdni" : {
+        "dcdn_id" : "",
+        "jwt_decoding_secret" : ""
     }
 }
diff --git 
a/traffic_ops/app/db/migrations/2022011112591400_added_cdni_capacity_table.down.sql
 
b/traffic_ops/app/db/migrations/2022011112591400_added_cdni_capacity_table.down.sql
new file mode 100644
index 0000000..5d3837e
--- /dev/null
+++ 
b/traffic_ops/app/db/migrations/2022011112591400_added_cdni_capacity_table.down.sql
@@ -0,0 +1,23 @@
+/*
+ * 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_total_limits;
+DROP TABLE IF EXISTS cdni_host_limits;
+DROP TABLE IF EXISTS cdni_telemetry_metrics;
+DROP TABLE IF EXISTS cdni_telemetry;
+DROP TABLE IF EXISTS cdni_footprints;
+DROP TABLE IF EXISTS cdni_capabilities;
diff --git 
a/traffic_ops/app/db/migrations/2022011112591400_added_cdni_capacity_table.up.sql
 
b/traffic_ops/app/db/migrations/2022011112591400_added_cdni_capacity_table.up.sql
new file mode 100644
index 0000000..7c542d0
--- /dev/null
+++ 
b/traffic_ops/app/db/migrations/2022011112591400_added_cdni_capacity_table.up.sql
@@ -0,0 +1,82 @@
+/*
+ * 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_capabilities (
+                                                 id bigserial NOT NULL,
+                                                 type text NOT NULL,
+                                                 ucdn text NOT NULL,
+                                                 last_updated timestamp with 
time zone DEFAULT now() NOT NULL,
+    CONSTRAINT pk_cdni_capabilities PRIMARY KEY (id)
+);
+
+CREATE TABLE IF NOT EXISTS cdni_footprints (
+                                               id bigserial NOT NULL,
+                                               footprint_type text NOT NULL,
+                                               footprint_value text[] NOT NULL,
+                                               ucdn text NOT NULL,
+                                               capability_id bigint NOT NULL,
+                                               last_updated timestamp with 
time zone DEFAULT now() NOT NULL,
+    CONSTRAINT pk_cdni_footprints PRIMARY KEY (id),
+    CONSTRAINT fk_cdni_footprint_capabilities FOREIGN KEY (capability_id) 
REFERENCES cdni_capabilities(id) ON UPDATE CASCADE ON DELETE CASCADE
+);
+
+CREATE TABLE IF NOT EXISTS cdni_telemetry (
+                                        id text NOT NULL,
+                                        type text NOT NULL,
+                                        capability_id bigint NOT NULL,
+                                        last_updated timestamp with time zone 
DEFAULT now() NOT NULL,
+    CONSTRAINT pk_cdni_telemetry PRIMARY KEY (id),
+    CONSTRAINT fk_cdni_telemetry_capabilities FOREIGN KEY (capability_id) 
REFERENCES cdni_capabilities(id) ON UPDATE CASCADE ON DELETE CASCADE
+);
+
+CREATE TABLE IF NOT EXISTS cdni_telemetry_metrics (
+                                        name text NOT NULL,
+                                        time_granularity bigint NOT NULL,
+                                        data_percentile bigint NOT NULL,
+                                        latency int NOT NULL,
+                                        telemetry_id text NOT NULL,
+                                        last_updated timestamp with time zone 
DEFAULT now() NOT NULL,
+    CONSTRAINT pk_cdni_telemetry_metrics PRIMARY KEY (name),
+    CONSTRAINT fk_cdni_telemetry_metrics_telemetry FOREIGN KEY (telemetry_id) 
REFERENCES cdni_telemetry(id) ON UPDATE CASCADE ON DELETE CASCADE
+);
+
+CREATE TABLE IF NOT EXISTS cdni_total_limits (
+                                        limit_type text NOT NULL,
+                                        maximum_hard bigint NOT NULL,
+                                        maximum_soft bigint NOT NULL,
+                                        telemetry_id text NOT NULL,
+                                        telemetry_metric text NOT NULL,
+                                        capability_id bigint NOT NULL,
+                                        last_updated timestamp with time zone 
DEFAULT now() NOT NULL,
+    CONSTRAINT pk_cdni_total_limits PRIMARY KEY (capability_id, telemetry_id),
+    CONSTRAINT fk_cdni_total_limits_telemetry FOREIGN KEY (telemetry_id) 
REFERENCES cdni_telemetry(id) ON UPDATE CASCADE ON DELETE CASCADE,
+    CONSTRAINT fk_cdni_total_limits_capabilities FOREIGN KEY (capability_id) 
REFERENCES cdni_capabilities(id) ON UPDATE CASCADE ON DELETE CASCADE
+);
+
+CREATE TABLE IF NOT EXISTS cdni_host_limits (
+                                        limit_type text NOT NULL,
+                                        maximum_hard bigint NOT NULL,
+                                        maximum_soft bigint NOT NULL,
+                                        telemetry_id text NOT NULL,
+                                        telemetry_metric text NOT NULL,
+                                        capability_id bigint NOT NULL,
+                                        host text NOT NULL,
+                                        last_updated timestamp with time zone 
DEFAULT now() NOT NULL,
+    CONSTRAINT pk_cdni_host_limits PRIMARY KEY (capability_id, telemetry_id, 
host),
+    CONSTRAINT fk_cdni_host_limits_telemetry FOREIGN KEY (telemetry_id) 
REFERENCES cdni_telemetry(id) ON UPDATE CASCADE ON DELETE CASCADE,
+    CONSTRAINT fk_cdni_total_limits_capabilities FOREIGN KEY (capability_id) 
REFERENCES cdni_capabilities(id) ON UPDATE CASCADE ON DELETE CASCADE
+);
diff --git a/traffic_ops/traffic_ops_golang/cdni/capacity.go 
b/traffic_ops/traffic_ops_golang/cdni/capacity.go
new file mode 100644
index 0000000..17f6cf0
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/cdni/capacity.go
@@ -0,0 +1,156 @@
+package cdni
+
+/*
+ * 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.
+ */
+
+import (
+       "fmt"
+
+       "github.com/apache/trafficcontrol/lib/go-log"
+       "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
+)
+
+func getCapacities(inf *api.APIInfo, ucdn string) (Capabilities, error) {
+       capRows, err := inf.Tx.Tx.Query(CapabilityQuery, FciCapacityLimits, 
ucdn)
+       if err != nil {
+               return Capabilities{}, fmt.Errorf("querying capabilities: %w", 
err)
+       }
+       defer log.Close(capRows, "closing capabilities query")
+       capabilities := []CapabilityQueryResponse{}
+       for capRows.Next() {
+               var capability CapabilityQueryResponse
+               if err := capRows.Scan(&capability.Id, &capability.Type, 
&capability.UCdn); err != nil {
+                       return Capabilities{}, fmt.Errorf("scanning db rows: 
%w", err)
+               }
+               capabilities = append(capabilities, capability)
+       }
+
+       footprintMap, err := getFootprintMap(inf.Tx.Tx)
+       if err != nil {
+               return Capabilities{}, err
+       }
+
+       totalLimitsMap, err := getTotalLimitsMap(inf.Tx.Tx)
+       if err != nil {
+               return Capabilities{}, err
+       }
+
+       hostLimitsMap, err := getHostLimitsMap(inf.Tx.Tx)
+       if err != nil {
+               return Capabilities{}, err
+       }
+
+       fciCaps := Capabilities{}
+
+       for _, cap := range capabilities {
+               fciCap := Capability{}
+               fciCap.Footprints = footprintMap[cap.Id]
+               if fciCap.Footprints == nil {
+                       fciCap.Footprints = []Footprint{}
+               }
+               totalLimits := totalLimitsMap[cap.Id]
+               if totalLimits == nil {
+                       totalLimits = []TotalLimitsQueryResponse{}
+               }
+               hostLimits := hostLimitsMap[cap.Id]
+               if hostLimits == nil {
+                       hostLimits = []HostLimitsResponse{}
+               }
+
+               returnedTotalLimits := []Limit{}
+               for _, tl := range totalLimits {
+                       returnedTotalLimit := Limit{
+                               LimitType:   CapacityLimitType(tl.LimitType),
+                               MaximumHard: tl.MaximumHard,
+                               MaximumSoft: tl.MaximumSoft,
+                               TelemetrySource: TelemetrySource{
+                                       Id:     tl.TelemetryId,
+                                       Metric: tl.TelemetryMetic,
+                               },
+                       }
+                       returnedTotalLimits = append(returnedTotalLimits, 
returnedTotalLimit)
+               }
+
+               returnedHostLimits := []HostLimit{}
+               hostToLimitMap := map[string][]Limit{}
+               for _, hl := range hostLimits {
+                       limit := Limit{
+                               LimitType:   CapacityLimitType(hl.LimitType),
+                               MaximumHard: hl.MaximumHard,
+                               MaximumSoft: hl.MaximumSoft,
+                               TelemetrySource: TelemetrySource{
+                                       Id:     hl.TelemetryId,
+                                       Metric: hl.TelemetryMetic,
+                               },
+                       }
+
+                       if val, ok := hostToLimitMap[hl.Host]; ok {
+                               val = append(val, limit)
+                               hostToLimitMap[hl.Host] = val
+                       } else {
+                               hlList := []Limit{}
+                               hlList = append(hlList, limit)
+                               hostToLimitMap[hl.Host] = hlList
+                       }
+               }
+
+               for h, l := range hostToLimitMap {
+                       returnedHostLimit := HostLimit{
+                               Host:   h,
+                               Limits: l,
+                       }
+                       returnedHostLimits = append(returnedHostLimits, 
returnedHostLimit)
+               }
+
+               fciCap.CapabilityType = FciCapacityLimits
+               fciCap.CapabilityValue = []CapacityCapabilityValue{
+                       {
+                               TotalLimits: returnedTotalLimits,
+                               HostLimits:  returnedHostLimits,
+                       },
+               }
+
+               fciCaps.Capabilities = append(fciCaps.Capabilities, fciCap)
+       }
+
+       return fciCaps, nil
+}
+
+type CapabilityQueryResponse struct {
+       Id   int    `json:"id" db:"id"`
+       Type string `json:"type" db:"type"`
+       UCdn string `json:"ucdn" db:"ucdn"`
+}
+
+type TotalLimitsQueryResponse struct {
+       LimitType      string `json:"limit_type" db:"limit_type"`
+       MaximumHard    int64  `json:"maximum_hard" db:"maximum_hard"`
+       MaximumSoft    int64  `json:"maximum_soft" db:"maximum_soft"`
+       TelemetryId    string `json:"telemetry_id" db:"telemetry_id"`
+       TelemetryMetic string `json:"telemetry_metric" db:"telemetry_metric"`
+       UCdn           string `json:"ucdn" db:"ucdn"`
+       Id             string `json:"id" db:"id"`
+       Type           string `json:"type" db:"type"`
+       Name           string `json:"name" db:"name"`
+       CapabilityId   int    `json:"-"`
+}
+type HostLimitsResponse struct {
+       Host string `json:"host" db:"host"`
+       TotalLimitsQueryResponse
+}
diff --git a/traffic_ops/traffic_ops_golang/cdni/shared.go 
b/traffic_ops/traffic_ops_golang/cdni/shared.go
new file mode 100644
index 0000000..83dee5f
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/cdni/shared.go
@@ -0,0 +1,334 @@
+package cdni
+
+/*
+ * 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.
+ */
+
+import (
+       "database/sql"
+       "errors"
+       "fmt"
+       "github.com/lib/pq"
+       "net/http"
+       "time"
+
+       "github.com/apache/trafficcontrol/lib/go-log"
+       "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
+
+       "github.com/dgrijalva/jwt-go"
+)
+
+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 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 = `
+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`
+
+func GetCapabilities(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()
+
+       bearerToken := r.Header.Get("Authorization")
+       if bearerToken == "" {
+               api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, 
errors.New("bearer token header is required"), nil)
+               return
+       }
+
+       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
+       }
+
+       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
+       }
+       if !token.Valid {
+               api.HandleErr(w, r, nil, http.StatusInternalServerError, 
errors.New("invalid token"), nil)
+               return
+       }
+
+       var expirationFloat float64
+       var ucdn string
+       var dcdn string
+       for key, val := range claims {
+               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
+                       }
+                       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
+                       }
+                       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
+                       }
+                       expirationFloat = val.(float64)
+               }
+       }
+
+       expiration := int64(expirationFloat)
+
+       if expiration < time.Now().Unix() {
+               api.HandleErr(w, r, nil, http.StatusForbidden, 
errors.New("token is expired"), nil)
+               return
+       }
+       if dcdn != inf.Config.Cdni.DCdnId {
+               api.HandleErr(w, r, nil, http.StatusForbidden, 
errors.New("invalid token - incorrect dcdn"), nil)
+               return
+       }
+       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
+       }
+
+       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 getFootprintMap(tx *sql.Tx) (map[int][]Footprint, error) {
+       footRows, err := tx.Query(AllFootprintQuery)
+       if err != nil {
+               return nil, fmt.Errorf("querying footprints: %w", err)
+       }
+       defer log.Close(footRows, "closing foorpint query")
+       footprintMap := map[int][]Footprint{}
+       for footRows.Next() {
+               var footprint Footprint
+               if err := footRows.Scan(&footprint.FootprintType, 
pq.Array(&footprint.FootprintValue), &footprint.CapabilityId); err != nil {
+                       return nil, fmt.Errorf("scanning db rows: %w", err)
+               }
+
+               footprintMap[footprint.CapabilityId] = 
append(footprintMap[footprint.CapabilityId], footprint)
+       }
+
+       return footprintMap, nil
+}
+
+func getTotalLimitsMap(tx *sql.Tx) (map[int][]TotalLimitsQueryResponse, error) 
{
+       tlRows, err := tx.Query(totalLimitsQuery)
+       if err != nil {
+               return nil, fmt.Errorf("querying total limits: %w", err)
+       }
+
+       defer log.Close(tlRows, "closing total capacity limits query")
+       totalLimitsMap := map[int][]TotalLimitsQueryResponse{}
+       for tlRows.Next() {
+               var totalLimit TotalLimitsQueryResponse
+               if err := tlRows.Scan(&totalLimit.LimitType, 
&totalLimit.MaximumHard, &totalLimit.MaximumSoft, &totalLimit.TelemetryId, 
&totalLimit.TelemetryMetic, &totalLimit.Id, &totalLimit.Type, &totalLimit.Name, 
&totalLimit.CapabilityId); err != nil {
+                       return nil, fmt.Errorf("scanning db rows: %w", err)
+               }
+
+               totalLimitsMap[totalLimit.CapabilityId] = 
append(totalLimitsMap[totalLimit.CapabilityId], totalLimit)
+       }
+
+       return totalLimitsMap, nil
+}
+
+func getHostLimitsMap(tx *sql.Tx) (map[int][]HostLimitsResponse, error) {
+       hlRows, err := tx.Query(hostLimitsQuery)
+       if err != nil {
+               return nil, fmt.Errorf("querying host limits: %w", err)
+       }
+
+       defer log.Close(hlRows, "closing host capacity limits query")
+       hostLimitsMap := map[int][]HostLimitsResponse{}
+       for hlRows.Next() {
+               var hostLimit HostLimitsResponse
+               if err := hlRows.Scan(&hostLimit.LimitType, 
&hostLimit.MaximumHard, &hostLimit.MaximumSoft, &hostLimit.TelemetryId, 
&hostLimit.TelemetryMetic, &hostLimit.Id, &hostLimit.Type, &hostLimit.Name, 
&hostLimit.Host, &hostLimit.CapabilityId); err != nil {
+                       return nil, fmt.Errorf("scanning db rows: %w", err)
+               }
+
+               hostLimitsMap[hostLimit.CapabilityId] = 
append(hostLimitsMap[hostLimit.CapabilityId], hostLimit)
+       }
+
+       return hostLimitsMap, nil
+}
+
+func getTelemetriesMap(tx *sql.Tx) (map[int][]Telemetry, error) {
+       rows, err := tx.Query(`SELECT id, type, capability_id FROM 
cdni_telemetry`)
+       if err != nil {
+               return nil, errors.New("querying cdni telemetry: " + 
err.Error())
+       }
+       defer log.Close(rows, "closing telemetry query")
+
+       telemetryMap := map[int][]Telemetry{}
+       for rows.Next() {
+               telemetry := Telemetry{}
+               if err := rows.Scan(&telemetry.Id, &telemetry.Type, 
&telemetry.CapabilityId); err != nil {
+                       return nil, errors.New("scanning telemetry: " + 
err.Error())
+               }
+
+               telemetryMap[telemetry.CapabilityId] = 
append(telemetryMap[telemetry.CapabilityId], telemetry)
+       }
+
+       return telemetryMap, nil
+}
+
+func getTelemetryMetricsMap(tx *sql.Tx) (map[string][]Metric, error) {
+       tmRows, err := tx.Query(`SELECT name, time_granularity, 
data_percentile, latency, telemetry_id FROM cdni_telemetry_metrics`)
+       if err != nil {
+               return nil, errors.New("querying cdni telemetry metrics: " + 
err.Error())
+       }
+       defer log.Close(tmRows, "closing telemetry metrics query")
+
+       telemetryMetricMap := map[string][]Metric{}
+       for tmRows.Next() {
+               metric := Metric{}
+               if err := tmRows.Scan(&metric.Name, &metric.TimeGranularity, 
&metric.DataPercentile, &metric.Latency, &metric.TelemetryId); err != nil {
+                       return nil, errors.New("scanning telemetry metric: " + 
err.Error())
+               }
+
+               telemetryMetricMap[metric.TelemetryId] = 
append(telemetryMetricMap[metric.TelemetryId], metric)
+       }
+
+       return telemetryMetricMap, nil
+}
+
+type Capabilities struct {
+       Capabilities []Capability `json:"capabilities"`
+}
+
+type Capability struct {
+       CapabilityType  SupportedCapabilities `json:"capability-type"`
+       CapabilityValue interface{}           `json:"capability-value"`
+       Footprints      []Footprint           `json:"footprints"`
+}
+
+type CapacityCapabilityValue struct {
+       TotalLimits []Limit     `json:"total-limits"`
+       HostLimits  []HostLimit `json:"host-limits"`
+}
+
+type HostLimit struct {
+       Host   string  `json:"host"`
+       Limits []Limit `json:"limits"`
+}
+
+type Limit struct {
+       LimitType       CapacityLimitType `json:"limit-type"`
+       MaximumHard     int64             `json:"maximum-hard"`
+       MaximumSoft     int64             `json:"maximum-soft"`
+       TelemetrySource TelemetrySource   `json:"telemetry-source"`
+}
+
+type TelemetrySource struct {
+       Id     string `json:"id"`
+       Metric string `json:"metric"`
+}
+
+type TelemetryCapabilityValue struct {
+       Sources []Telemetry `json:"sources"`
+}
+
+type Telemetry struct {
+       Id           string              `json:"id"`
+       Type         TelemetrySourceType `json:"type"`
+       CapabilityId int                 `json:"-"`
+       Metrics      []Metric            `json:"metrics"`
+}
+
+type Metric struct {
+       Name            string `json:"name"`
+       TimeGranularity int    `json:"time-granularity"`
+       DataPercentile  int    `json:"data-percentile"`
+       Latency         int    `json:"latency"`
+       TelemetryId     string `json:"-"`
+}
+
+type Footprint struct {
+       FootprintType  FootprintType `json:"footprint-type" db:"footprint_type"`
+       FootprintValue []string      `json:"footprint-value" 
db:"footprint_value"`
+       CapabilityId   int           `json:"-"`
+}
+
+type CapacityLimitType string
+
+const (
+       Egress         CapacityLimitType = "egress"
+       Requests                         = "requests"
+       StorageSize                      = "storage-size"
+       StorageObjects                   = "storage-objects"
+       Sessions                         = "sessions"
+       CacheSize                        = "cache-size"
+)
+
+type SupportedCapabilities string
+
+const (
+       FciTelemetry      SupportedCapabilities = "FCI.Telemetry"
+       FciCapacityLimits                       = "FCI.CapacityLimits"
+)
+
+type TelemetrySourceType string
+
+const (
+       Generic TelemetrySourceType = "generic"
+)
+
+type FootprintType string
+
+const (
+       Ipv4Cidr    FootprintType = "ipv4cidr"
+       Ipv6Cidr                  = "ipv6cidr"
+       Asn                       = "asn"
+       CountryCode               = "countrycode"
+)
diff --git a/traffic_ops/traffic_ops_golang/cdni/telemetry.go 
b/traffic_ops/traffic_ops_golang/cdni/telemetry.go
new file mode 100644
index 0000000..6fd718c
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/cdni/telemetry.go
@@ -0,0 +1,94 @@
+package cdni
+
+/*
+ * 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.
+ */
+
+import (
+       "fmt"
+
+       "github.com/apache/trafficcontrol/lib/go-log"
+       "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
+)
+
+func getTelemetries(inf *api.APIInfo, ucdn string) (Capabilities, error) {
+       capRows, err := inf.Tx.Tx.Query(CapabilityQuery, FciTelemetry, ucdn)
+       if err != nil {
+               return Capabilities{}, fmt.Errorf("querying capabilities: %w", 
err)
+       }
+       defer log.Close(capRows, "closing capabilities query")
+       capabilities := []CapabilityQueryResponse{}
+       for capRows.Next() {
+               var capability CapabilityQueryResponse
+               if err := capRows.Scan(&capability.Id, &capability.Type, 
&capability.UCdn); err != nil {
+                       return Capabilities{}, fmt.Errorf("scanning db rows: 
%w", err)
+               }
+               capabilities = append(capabilities, capability)
+       }
+
+       footprintMap, err := getFootprintMap(inf.Tx.Tx)
+       if err != nil {
+               return Capabilities{}, err
+       }
+
+       telemetryMap, err := getTelemetriesMap(inf.Tx.Tx)
+       if err != nil {
+               return Capabilities{}, err
+       }
+
+       telemetryMetricMap, err := getTelemetryMetricsMap(inf.Tx.Tx)
+       if err != nil {
+               return Capabilities{}, err
+       }
+
+       fciCaps := Capabilities{}
+
+       for _, cap := range capabilities {
+               fciCap := Capability{}
+               fciCap.Footprints = footprintMap[cap.Id]
+               if fciCap.Footprints == nil {
+                       fciCap.Footprints = []Footprint{}
+               }
+               returnList := []Telemetry{}
+               telemetryList := telemetryMap[cap.Id]
+               if telemetryList == nil {
+                       telemetryList = []Telemetry{}
+               }
+
+               for _, t := range telemetryList {
+                       telemetryMetricsList := telemetryMetricMap[t.Id]
+                       if telemetryMetricsList == nil {
+                               telemetryMetricsList = []Metric{}
+                       }
+                       t.Metrics = telemetryMetricsList
+                       returnList = append(returnList, t)
+               }
+
+               telemetry := Capability{
+                       CapabilityType: FciTelemetry,
+                       CapabilityValue: TelemetryCapabilityValue{
+                               Sources: returnList,
+                       },
+                       Footprints: fciCap.Footprints,
+               }
+
+               fciCaps.Capabilities = append(fciCaps.Capabilities, telemetry)
+       }
+
+       return fciCaps, nil
+}
diff --git a/traffic_ops/traffic_ops_golang/config/config.go 
b/traffic_ops/traffic_ops_golang/config/config.go
index 48914e8..3d7f108 100644
--- a/traffic_ops/traffic_ops_golang/config/config.go
+++ b/traffic_ops/traffic_ops_golang/config/config.go
@@ -62,6 +62,7 @@ type Config struct {
        UseIMS                  bool                    `json:"use_ims"`
        RoleBasedPermissions    bool                    
`json:"role_based_permissions"`
        DefaultCertificateInfo  *DefaultCertificateInfo 
`json:"default_certificate_info"`
+       Cdni                    *CdniConf               `json:"cdni"`
 }
 
 // ConfigHypnotoad carries http setting for hypnotoad (mojolicious) server
@@ -237,6 +238,11 @@ type ConfigInflux struct {
        Secure      *bool  `json:"secure"`
 }
 
+type CdniConf struct {
+       DCdnId            string `json:"dcdn_id"`
+       JwtDecodingSecret string `json:"jwt_decoding_secret"`
+}
+
 // NewFakeConfig returns a fake Config struct with just enough data to view 
Routes.
 func NewFakeConfig() Config {
        c := Config{}
diff --git a/traffic_ops/traffic_ops_golang/routing/routes.go 
b/traffic_ops/traffic_ops_golang/routing/routes.go
index 6a5e362..8252faa 100644
--- a/traffic_ops/traffic_ops_golang/routing/routes.go
+++ b/traffic_ops/traffic_ops_golang/routing/routes.go
@@ -44,6 +44,7 @@ import (
        "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/cdn"
        
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/cdn_lock"
        
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/cdnfederation"
+       "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/cdni"
        
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/cdnnotification"
        
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/coordinate"
        
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/crconfig"
@@ -130,6 +131,9 @@ func Routes(d ServerData) ([]Route, http.Handler, error) {
                 * 4.x API
                 */
 
+               // 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},
+
                // 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},
 

Reply via email to