Skip to content

Commit

Permalink
feat: Open SNOW ticket API (#1827)
Browse files Browse the repository at this point in the history
* feat: Open SNOW ticket API
  • Loading branch information
aleixhub authored Apr 18, 2024
1 parent 3f86615 commit f88efdf
Show file tree
Hide file tree
Showing 21 changed files with 241 additions and 15 deletions.
2 changes: 2 additions & 0 deletions admin/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export DB_USERNAME=<<REDATECT>>
export DB_PASSWORD=<<REDACTED>>
export DB_NAME=<<REDACTED>>
export DB_PORT=<<REDACTED>>
export SERVICENOW_AUTH_KEY=<<REDACTED>>
export SERVICENOW_FORM_ID=<<REDACTED>>
-----------------------------------------------

Commands each time to start:
Expand Down
3 changes: 2 additions & 1 deletion admin/api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from babylon import Babylon
from routers import incidents
from routers import incidents, support
from models import Database as db
from models.custom_base import create_tables
from datetime import datetime
Expand Down Expand Up @@ -69,3 +69,4 @@ async def log_access(request: Request, call_next):

# Including Routers
app.include_router(incidents.router)
app.include_router(support.router)
5 changes: 3 additions & 2 deletions admin/api/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@ async def startup():
DB_NAME
DB_HOSTNAME
DB_PORT
SERVICENOW_AUTH_KEY
SERVICENOW_FORM_ID
"""

# Define a list with the environment variables you need to check
required_env_vars = ["DB_USERNAME",
"DB_PASSWORD", "DB_NAME", "DB_HOSTNAME"]
required_env_vars = ["DB_USERNAME", "DB_PASSWORD", "DB_NAME", "DB_HOSTNAME", "SERVICENOW_AUTH_KEY", "SERVICENOW_FORM_ID"]

# Check if all environment variables exist
for var in required_env_vars:
Expand Down
1 change: 1 addition & 0 deletions admin/api/routers/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from .support import router as support_router
from .incidents import router as incidents_router
83 changes: 83 additions & 0 deletions admin/api/routers/support.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
from typing import List
import logging
import json
from fastapi import APIRouter, HTTPException, Depends
from fastapi.responses import JSONResponse
from schemas import SupportCreate, SupportResponse
import aiohttp
import asyncio
import os

logger = logging.getLogger('babylon-api')

tags = ["support"]

router = APIRouter(tags=tags)

# Service Now params:
SERVICENOW_AUTH_SECRET = os.getenv('SERVICENOW_AUTH_SECRET')
SERVICENOW_FORM_ID = os.getenv('SERVICENOW_FORM_ID')
# WORKSHOP_FORM_URL = f"https://redhat.service-now.com/api/sn_sc/v1/servicecatalog/items/{SERVICENOW_FORM_ID}/order_now"
WORKSHOP_FORM_URL = f"https://redhatqa.service-now.com/api/sn_sc/v1/servicecatalog/items/{SERVICENOW_FORM_ID}/order_now"
SYS_USER_URL = "https://redhatqa.service-now.com/api/now/table/sys_user"
API_HEADERS={"Authorization": f"Basic {SERVICENOW_AUTH_SECRET}"}

async def create_ticket(support_create):
async with aiohttp.ClientSession(headers=API_HEADERS) as session:
async with session.post(WORKSHOP_FORM_URL, json=support_create) as response:
return await response.json()

async def get_user_sys_id(email):
async with aiohttp.ClientSession(headers=API_HEADERS) as session:
params = {"email": email}
async with session.get(SYS_USER_URL, params=params) as response:
return await response.json()


@router.post("/api/admin/v1/workshop/support",
response_model=SupportResponse,
summary="Create support ticket")
async def create_support_ticket(support_create: SupportCreate):
try:
users_result = await get_user_sys_id(support_create.email)
user_sys_id = users_result["result"][0]["sys_id"]
logger.info(user_sys_id)
support_create = create_support_request_json(support_create, user_sys_id)
ticket = await create_ticket(support_create)
logger.info(ticket)
return {
"sys_id": ticket["result"]["sys_id"],
"request_number": ticket["result"]["request_number"],
"request_id": ticket["result"]["request_id"]
}
except Exception as e:
logger.error(f"Error creating support ticket: {e}", stack_info=True)
raise HTTPException(status_code=500, detail="Error support ticket. Contact the administrator") from e


def create_support_request_json(support_create, user_sys_id):
return {
"sysparm_quantity":1,
"variables":{
"number_of_attendees":f"{support_create.number_of_attendees}",
"provide_additional_details":"Auto-Generated by demo.redhat.com portal",
"workshop_or_demo_name":f"{support_create.name}",
"workshop_or_demo_start_date":f"{support_create.start_time}",
"what_is_the_sfdc_opportunity":f"{support_create.sfdc}",
"general_event_name":f"{support_create.event_name}",
"other_facilitators_e_mail_addresses":f"{support_create.email}",
"universal_watch_list":f"{support_create.email}",
"will_you_be_performing_initial_setup_of_your_environment_or_do_you_require_assistance":"i_will_perform_setup",
"provide_your_guid_or_the_url_from_your_browser_linking_to_your_workshop_service":f"{support_create.url}",
"requested_for_rf":"true",
"workshop_or_demo_end_date_and_time":f"{support_create.end_time}",
"number_of_attendees_demo":f"{support_create.number_of_attendees}",
"what_do_you_need_help_with":"i_need_help_ with_a_future_demo_or_workshop",
"workshop_or_demo_start_date_and_time":f"{support_create.start_time}",
"email":f"{support_create.email}",
"do_you_need_to_remove_auto_stop":"No",
"requested_for": f"{user_sys_id}",
},
"get_portal_messages":"true",
"sysparm_no_validation":"true"
}
1 change: 1 addition & 0 deletions admin/api/schemas/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from .incidents import IncidentSchema, StatusParams, IncidentStatus, IncidentCreate
from .support import SupportCreate, SupportResponse
23 changes: 23 additions & 0 deletions admin/api/schemas/support.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from typing import List, Optional, Literal
import logging
from pydantic import BaseModel
from datetime import datetime


logger = logging.getLogger('babylon-api')


class SupportCreate(BaseModel):
number_of_attendees: int
sfdc: str
name: str
event_name: str
url: str
start_time: datetime
end_time: datetime
email: str

class SupportResponse(BaseModel):
sys_id: str
request_number: str
request_id: str
4 changes: 2 additions & 2 deletions admin/helm/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ apiVersion: v2
name: babylon-admin
description: A Helm chart for the babylon admin component.
type: application
version: 1.0.3
appVersion: 1.0.3
version: 1.0.4
appVersion: 1.0.4
10 changes: 10 additions & 0 deletions admin/helm/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@ spec:
name: {{ default .Values.db.secretName "database" }}
- name: DB_PORT
value: {{ default .Values.db.port "54327" |quote }}
- name: SERVICENOW_AUTH_KEY
valueFrom:
secretKeyRef:
key: authKey
name: {{ .Values.servicenow.secretName | default "babylon-admin-servicenow" }}
- name: SERVICENOW_FORM_ID
valueFrom:
secretKeyRef:
key: workshopFormId
name: {{ .Values.servicenow.secretName | default "babylon-admin-servicenow" }}
image: {{ include "babylon-admin.image" . | quote }}
imagePullPolicy: {{ .Values.image.pullPolicy }}
resources:
Expand Down
27 changes: 27 additions & 0 deletions admin/helm/templates/servicenow-secret.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{{- if .Values.servicenow.deploy }}
apiVersion: bitwarden-k8s-secrets-manager.demo.redhat.com/v1
kind: BitwardenSyncSecret
metadata:
name: {{ .Values.servicenow.secretName | default "babylon-admin-servicenow" }}
namespace: {{ include "babylon-admin.namespaceName" . }}
spec:
data:
authKey:
secret: service_now
key: auth_key
workshopFormId:
secret: service_now
key: workshop_form_id
{{- else }}
---
apiVersion: v1
kind: Secret
metadata:
name: {{ .Values.servicenow.secretName | default "babylon-admin-servicenow" }}
namespace: {{ include "babylon-admin.namespaceName" . }}
labels:
{{- include "babylon-admin.labels" . | nindent 4 }}
data:
authKey: {{ required ".Values.servicenow.authKey is required!" .Values.servicenow.authKey | b64enc }}
workshopFormId: {{ required ".Values.servicenow.workshopFormId is required!" .Values.servicenow.workshopFormId | b64enc }}
{{- end }}
6 changes: 6 additions & 0 deletions admin/helm/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ db:
port: 54327
secretName: database

servicenow:
# authKey: ''
deploy: true
workshopFormId: b48fe3cc870b2d508a51bbbf8bbb3576
secretName: servicenow

imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
Expand Down
2 changes: 2 additions & 0 deletions admin/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ kubernetes-asyncio==28.2.0
psycopg2==2.9.9
pydantic==2.4.2
uvicorn==0.23.2
aiohttp==3.9.4
asyncio==3.4.3
4 changes: 2 additions & 2 deletions agnosticv-operator/helm/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ apiVersion: v2
name: babylon-agnosticv-operator
description: Operator for managing babylon configuraion from AgnosticV repositories
type: application
version: 1.3.8
appVersion: 1.3.8
version: 1.3.9
appVersion: 1.3.9
1 change: 1 addition & 0 deletions agnosticv-operator/operator/agnosticvcomponent.py
Original file line number Diff line number Diff line change
Expand Up @@ -1144,6 +1144,7 @@ async def __update_catalog_item(self, current_state, definition, logger, retries
if (
not annotation.startswith(f"{Babylon.catalog_api_group}/") or
annotation in (
"babylon.gpte.redhat.com/servicenow",
"babylon.gpte.redhat.com/ops",
"babylon.gpte.redhat.com/totalRatings",
)
Expand Down
5 changes: 5 additions & 0 deletions catalog/api/wsgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -785,6 +785,11 @@ def update_incident(incident_id):
data = flask.request.get_json()
return api_proxy(method="POST", url=f"{admin_api}/api/admin/v1/incidents/{incident_id}", data=json.dumps(data), headers=flask.request.headers)

@application.route("/api/admin/workshop/support", methods=['POST'])
def create_support():
data = flask.request.get_json()
return api_proxy(method="POST", url=f"{admin_api}/api/admin/v1/workshop/support", data=json.dumps(data), headers=flask.request.headers)

@application.route("/api/workshop/<workshop_id>", methods=['GET'])
def workshop_get(workshop_id):
"""
Expand Down
2 changes: 1 addition & 1 deletion catalog/helm/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ api:
threads: 1
image:
#override:
tag: v0.12.7
tag: v0.12.8
repository: quay.io/redhat-gpte/babylon-catalog-api
pullPolicy: IfNotPresent
imagePullSecrets: []
Expand Down
31 changes: 27 additions & 4 deletions catalog/ui/src/app/Catalog/CatalogItemForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
createWorkshop,
createWorkshopProvision,
fetcher,
openWorkshopSupportTicket,
} from '@app/api';
import { CatalogItem, TPurposeOpts } from '@app/types';
import { displayName, isLabDeveloper, randomString } from '@app/util';
Expand All @@ -53,6 +54,7 @@ import AutoStopDestroy from '@app/components/AutoStopDestroy';
import CatalogItemFormAutoStopDestroyModal, { TDates, TDatesTypes } from './CatalogItemFormAutoStopDestroyModal';
import { formatCurrency, getEstimatedCost, isAutoStopDisabled } from './catalog-utils';
import ErrorBoundaryPage from '@app/components/ErrorBoundaryPage';
import useImpersonateUser from '@app/utils/useImpersonateUser';

import './catalog-item-form.css';

Expand All @@ -64,7 +66,12 @@ const CatalogItemFormData: React.FC<{ catalogItemName: string; catalogNamespaceN
const debouncedApiFetch = useDebounce(apiFetch, 1000);
const [autoStopDestroyModal, openAutoStopDestroyModal] = useState<TDatesTypes>(null);
const [isLoading, setIsLoading] = useState(false);
const { isAdmin, groups, roles, serviceNamespaces, userNamespace } = useSession().getSession();
const { isAdmin, groups, roles, serviceNamespaces, userNamespace, email } = useSession().getSession();
const { userImpersonated } = useImpersonateUser();
let userEmail = email;
if (userImpersonated) {
userEmail = userImpersonated;
}
const { data: catalogItem } = useSWRImmutable<CatalogItem>(
apiPaths.CATALOG_ITEM({ namespace: catalogNamespaceName, name: catalogItemName }),
fetcher
Expand Down Expand Up @@ -171,6 +178,7 @@ const CatalogItemFormData: React.FC<{ catalogItemName: string; catalogNamespaceN
...(scheduled !== null ? { endDate: scheduled.endDate } : { endDate: formState.endDate }),
...(scheduled !== null ? { startDate: scheduled.startDate } : {}),
});
const redirectUrl = `/workshops/${workshop.metadata.namespace}/${workshop.metadata.name}`;
await createWorkshopProvision({
catalogItem: catalogItem,
concurrency: provisionConcurrency,
Expand All @@ -179,8 +187,23 @@ const CatalogItemFormData: React.FC<{ catalogItemName: string; catalogNamespaceN
startDelay: provisionStartDelay,
workshop: workshop,
});

navigate(`/workshops/${workshop.metadata.namespace}/${workshop.metadata.name}`);
if (scheduled !== null) {
const today = new Date();
const twoWeeks = new Date(new Date().setDate(today.getDate() + 14));
if (scheduled.startDate >= twoWeeks) {
await openWorkshopSupportTicket(workshop, {
number_of_attendees: provisionCount,
sfdc: formState.salesforceId.value,
name: catalogItemName,
event_name: displayName,
url: `${window.location.origin}${redirectUrl}`,
start_date: scheduled.startDate,
end_date: scheduled.endDate,
email: userEmail,
});
}
}
navigate(redirectUrl);
} else {
const resourceClaim = await createServiceRequest({
catalogItem,
Expand Down Expand Up @@ -780,7 +803,7 @@ const CatalogItemFormData: React.FC<{ catalogItemName: string; catalogNamespaceN
</Button>
</ActionListItem>

{isAdmin || isLabDeveloper(groups) ? (
{isAdmin || isLabDeveloper(groups) || formState.workshop ? (
<ActionListItem>
<Button
isAriaDisabled={!submitRequestEnabled}
Expand Down
34 changes: 34 additions & 0 deletions catalog/ui/src/app/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,39 @@ export async function createWorkshopProvision({
return await createK8sObject(definition);
}

export async function openWorkshopSupportTicket(
workshop: Workshop,
{ number_of_attendees, sfdc, name, event_name, url, start_date, end_date, email }
) {
function date_to_time(date: Date) {
const offset = date.getTimezoneOffset();
date = new Date(date.getTime() - offset * 60 * 1000);
const d = date.toISOString().split('T')[0];
const hh = date.toISOString().split('T')[1].split(':')[0];
const mm = date.toISOString().split('T')[1].split(':')[1];
return `${d} ${hh}:${mm}`;
}
const resp = await apiFetch(apiPaths.WORKSHOP_SUPPORT({}), {
body: JSON.stringify({
number_of_attendees,
sfdc,
name,
event_name,
url,
start_time: date_to_time(start_date),
end_time: date_to_time(end_date),
email,
}),
headers: {
'Content-Type': 'application/json',
},
method: 'POST',
});
const workshopSuport = await resp.json();
workshop.metadata.annotations[`${BABYLON_DOMAIN}/servicenow`] = JSON.stringify(workshopSuport);
return await updateWorkshop(workshop);
}

export async function getApiSession(forceRefresh = false) {
const sessionPromise = window.sessionPromiseInstance;
let session: Session;
Expand Down Expand Up @@ -1731,4 +1764,5 @@ export const apiPaths: { [key in ResourceType]: (args: any) => string } = {
RATINGS_HISTORY: ({ assetUuid }: { assetUuid: string }) => `/api/ratings/catalogitem/${assetUuid}/history`,
RATING: ({ requestUid }: { requestUid: string }) => `/api/ratings/request/${requestUid}`,
USER_RATING: ({ requestUid }: { requestUid: string }) => `/api/ratings/request/${requestUid}`,
WORKSHOP_SUPPORT: () => `/api/admin/workshop/support`,
};
3 changes: 2 additions & 1 deletion catalog/ui/src/app/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -571,7 +571,8 @@ export type ResourceType =
| 'INCIDENT'
| 'RATING'
| 'RATINGS_HISTORY'
| 'USER_RATING';
| 'USER_RATING'
| 'WORKSHOP_SUPPORT';

export type ServiceActionActions = 'start' | 'stop' | 'delete' | 'rate' | 'retirement';

Expand Down
Loading

0 comments on commit f88efdf

Please sign in to comment.