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/10 23:13:05 UTC
[trafficcontrol] branch master updated: Added CDNi Capacity and Telemetry (#6524)
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 <33...@users.noreply.github.com>
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},