blob: 4cdec56b74b7d43f9c4ba3fc2cec34cf2a135c5e [file] [log] [blame]
#!/usr/bin/env bash
# Copyright 2018-present Open Networking Foundation
#
# 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.
# service_scaffold.sh
# creates directories and scaffolding for a service
set -e -o pipefail
LICENSE_TEXT=$(cat <<EOF
# Copyright $(date +%Y)-present Open Networking Foundation
#
# 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.
EOF
)
TPL_LICENSE_TEXT=$(cat <<EOF
{{/*
Copyright $(date +%Y)-present Open Networking Foundation
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.
*/}}
EOF
)
if [ -z "${SERVICE_NAME}" ];
then
SERVICE_NAME=$(basename "${PWD}")
echo "SERVICE_NAME undefined, using parent directory name: ${SERVICE_NAME}"
fi
if [ -z "${SERVICE_VERSION}" ];
then
SERVICE_VERSION="0.0.1"
echo "SERVICE_VERSION undefined, using default: ${SERVICE_VERSION}"
fi
echo "Creating service named: ${SERVICE_NAME}, version: ${SERVICE_VERSION}"
echo "${SERVICE_VERSION}" > VERSION
# create uppercase version of SERVICE_NAME in a naive fashion
UC_SERVICE_NAME=$(echo "${SERVICE_NAME}" | cut -c1 | tr '[:lower:]' '[:upper:]')$(echo "${SERVICE_NAME}" | cut -c2-)
echo "Uppercase service name: ${UC_SERVICE_NAME}"
echo "Creating README.md"
echo "# ${UC_SERVICE_NAME} Service" > README.md
echo "Creating directories"
mkdir -p xos/synchronizer/model_policies/
mkdir -p xos/synchronizer/models/
mkdir -p xos/synchronizer/pull_steps/
mkdir -p xos/synchronizer/steps/
mkdir -p xos/synchronizer/tests/
mkdir -p "helm-charts/${SERVICE_NAME}/templates"
echo "Creating empty model-deps"
echo "{}" > xos/synchronizer/model-deps
echo "Creating xproto scaffold"
cat << EOF > "xos/synchronizer/models/${SERVICE_NAME}.xproto"
option app_label = "${SERVICE_NAME}";
option name = "${SERVICE_NAME}";
message ${UC_SERVICE_NAME}Service (Service){
option verbose_name = "${UC_SERVICE_NAME} Service";
}
message ${UC_SERVICE_NAME}ServiceInstance (ServiceInstance){
option owner_class_name = "${UC_SERVICE_NAME}Service";
option verbose_name = "${UC_SERVICE_NAME} Service Instance";
}
EOF
echo "Creating unittest.cfg"
cat << EOF > "xos/unittest.cfg"
[unittest]
plugins=nose2.plugins.junitxml
code-directories=synchronizer
model_policies
steps
pull_steps
event_steps
EOF
echo "Creating service config.yaml"
cat << EOF > "xos/synchronizer/config.yaml"
${LICENSE_TEXT}
name: ${SERVICE_NAME}
required_models:
- ${UC_SERVICE_NAME}Service
- ${UC_SERVICE_NAME}ServiceInstance
dependency_graph: "/opt/xos/synchronizers/${SERVICE_NAME}/model-deps"
model_policies_dir: "/opt/xos/synchronizers/${SERVICE_NAME}/model_policies"
models_dir: "/opt/xos/synchronizers/${SERVICE_NAME}/models"
pull_steps_dir: "/opt/xos/synchronizers/${SERVICE_NAME}/pull_steps"
steps_dir: "/opt/xos/synchronizers/${SERVICE_NAME}/steps"
sys_dir: "/opt/xos/synchronizers/${SERVICE_NAME}/sys"
EOF
echo "Creating test_config.yaml"
cat << EOF > "xos/synchronizer/test_config.yaml"
${LICENSE_TEXT}
name: ${SERVICE_NAME}-testconfig
accessor:
username: xosadmin@opencord.org
password: "sample"
kind: "testframework"
logging:
version: 1
handlers:
console:
class: logging.StreamHandler
loggers:
'multistructlog':
handlers:
- console
EOF
echo "Creating synchronizer.py"
cat << EOF > "xos/synchronizer/${SERVICE_NAME}-synchronizer.py"
#!/usr/bin/env python
${LICENSE_TEXT}
"""
${SERVICE_NAME}-synchronizer.py
This is the main entrypoint for the synchronizer. It loads the config file, and
then starts the synchronizer.
"""
import importlib
import os
import sys
from xosconfig import Config
config_file = os.path.abspath(os.path.dirname(
os.path.realpath(__file__)) + '/config.yaml')
base_config_file = os.path.abspath(os.path.dirname(
os.path.realpath(__file__)) + '/config.yaml')
mounted_config_file = os.path.abspath(os.path.dirname(
os.path.realpath(__file__)) + '/mounted_config.yaml')
if os.path.isfile(mounted_config_file):
Config.init(base_config_file, 'synchronizer-config-schema.yaml',
mounted_config_file)
else:
Config.init(base_config_file, 'synchronizer-config-schema.yaml')
synchronizer_path = os.path.join(os.path.dirname(
os.path.realpath(__file__)), "../../synchronizers/new_base")
sys.path.append(synchronizer_path)
mod = importlib.import_module("xos-synchronizer")
mod.main()
EOF
chmod +x "xos/synchronizer/${SERVICE_NAME}-synchronizer.py"
echo "Creating synchronizer Dockerfile"
cat << EOF > Dockerfile.synchronizer
${LICENSE_TEXT}
# xosproject/${SERVICE_NAME}
FROM xosproject/xos-synchronizer-base:2.0.0
COPY xos/synchronizer /opt/xos/synchronizers/${SERVICE_NAME}
COPY VERSION /opt/xos/synchronizers/${SERVICE_NAME}/
ENTRYPOINT []
WORKDIR "/opt/xos/synchronizers/${SERVICE_NAME}"
# Label image
ARG org_label_schema_schema_version=1.0
ARG org_label_schema_name=${SERVICE_NAME}
ARG org_label_schema_version=unknown
ARG org_label_schema_vcs_url=unknown
ARG org_label_schema_vcs_ref=unknown
ARG org_label_schema_build_date=unknown
ARG org_opencord_vcs_commit_date=unknown
ARG org_opencord_component_chameleon_version=unknown
ARG org_opencord_component_chameleon_vcs_url=unknown
ARG org_opencord_component_chameleon_vcs_ref=unknown
ARG org_opencord_component_xos_version=unknown
ARG org_opencord_component_xos_vcs_url=unknown
ARG org_opencord_component_xos_vcs_ref=unknown
LABEL org.label-schema.schema-version=\$org_label_schema_schema_version \\
org.label-schema.name=\$org_label_schema_name \\
org.label-schema.version=\$org_label_schema_version \\
org.label-schema.vcs-url=\$org_label_schema_vcs_url \\
org.label-schema.vcs-ref=\$org_label_schema_vcs_ref \\
org.label-schema.build-date=\$org_label_schema_build_date \\
org.opencord.vcs-commit-date=\$org_opencord_vcs_commit_date \\
org.opencord.component.chameleon.version=\$org_opencord_component_chameleon_version \\
org.opencord.component.chameleon.vcs-url=\$org_opencord_component_chameleon_vcs_url \\
org.opencord.component.chameleon.vcs-ref=\$org_opencord_component_chameleon_vcs_ref \\
org.opencord.component.xos.version=\$org_opencord_component_xos_version \\
org.opencord.component.xos.vcs-url=\$org_opencord_component_xos_vcs_url \\
org.opencord.component.xos.vcs-ref=\$org_opencord_component_xos_vcs_ref
CMD ["/usr/bin/python", "/opt/xos/synchronizers/${SERVICE_NAME}/${SERVICE_NAME}-synchronizer.py"]
EOF
echo "Creating sync step"
cat << EOF > "xos/synchronizer/steps/sync_${SERVICE_NAME}_serviceinstance.py"
${LICENSE_TEXT}
from synchronizers.new_base.syncstep import SyncStep
from synchronizers.new_base.modelaccessor import ${UC_SERVICE_NAME}ServiceInstance
from xosconfig import Config
from multistructlog import create_logger
log = create_logger(Config().get('logging'))
class Sync${UC_SERVICE_NAME}ServiceInstance(SyncStep):
"""
Sync${UC_SERVICE_NAME}ServiceInstance
Implements sync step for syncing ${UC_SERVICE_NAME} Services
"""
provides = [${UC_SERVICE_NAME}ServiceInstance]
observes = [${UC_SERVICE_NAME}ServiceInstance]
requested_interval = 0
def sync_record(self, model):
log.info("Synchronizing ${UC_SERVICE_NAME}ServiceInstance",
object=str(model))
# TODO: Implement sync step
# Verify that the name is not empty, used in tests
if model.name != "":
model.save()
else:
raise Exception("Empty names aren't allowed")
def delete_record(self, model):
log.info("Deleting ${UC_SERVICE_NAME}ServiceInstance",
object=str(model))
# TODO: Implement delete step
EOF
echo "Creating test for synchronizer of service instance"
cat << EOF > "xos/synchronizer/steps/test_sync_${SERVICE_NAME}_serviceinstance.py"
${LICENSE_TEXT}
import os
import sys
import unittest
from mock import Mock
# Hack to load synchronizer framework
test_path = os.path.abspath(os.path.dirname(os.path.realpath(__file__)))
xos_dir = os.path.join(test_path, "../../..")
if not os.path.exists(os.path.join(test_path, "new_base")):
xos_dir = os.path.join(test_path, "../../../../../../orchestration/xos/xos")
services_dir = os.path.join(xos_dir, "../../xos-services")
sys.path.append(xos_dir)
sys.path.append(os.path.join(xos_dir, 'synchronizers', 'new_base'))
# END Hack to load synchronizer framework
# generate model from xproto
def get_models_fn(service_name, xproto_name):
name = os.path.join(service_name, "xos", xproto_name)
if os.path.exists(os.path.join(services_dir, name)):
return name
else:
name = os.path.join(service_name, "xos", "synchronizer", "models", xproto_name)
if os.path.exists(os.path.join(services_dir, name)):
return name
raise Exception("Unable to find service=%s xproto=%s" % (service_name, xproto_name))
# END generate model from xproto
class TestSync${UC_SERVICE_NAME}ServiceInstance(unittest.TestCase):
def setUp(self):
self.sys_path_save = sys.path
sys.path.append(xos_dir)
sys.path.append(os.path.join(xos_dir, 'synchronizers', 'new_base'))
# Setting up the config module
from xosconfig import Config
config = os.path.join(test_path, "../test_config.yaml")
Config.clear()
Config.init(config, "synchronizer-config-schema.yaml")
# END Setting up the config module
from synchronizers.new_base.mock_modelaccessor_build import build_mock_modelaccessor
build_mock_modelaccessor(xos_dir, services_dir, [
get_models_fn("${SERVICE_NAME}", "${SERVICE_NAME}.xproto"),
])
from synchronizers.new_base.modelaccessor import model_accessor
from sync_${SERVICE_NAME}_serviceinstance import Sync${UC_SERVICE_NAME}ServiceInstance
# import all class names to globals
for (k, v) in model_accessor.all_model_classes.items():
globals()[k] = v
self.sync_step = Sync${UC_SERVICE_NAME}ServiceInstance
# create a mock instance instance
self.model = Mock()
self.model.name = "Example"
def tearDown(self):
self.model = None
sys.path = self.sys_path_save
# TODO - The following two tests are very simple, replace with more meaningful ones
def test_save(self):
# Tests that the model can be saved
self.model.name = "${SERVICE_NAME}_test"
self.sync_step().sync_record(self.model)
self.model.save.assert_called()
def test_sync_rejected(self):
# Tests that an empty name raises an exception
self.model.name = ""
with self.assertRaises(Exception):
self.sync_step().sync_record(self.model)
if __name__ == '__main__':
unittest.main()
EOF
echo "Creating model policy for instance"
cat << EOF > "xos/synchronizer/model_policies/model_policy_${SERVICE_NAME}_serviceinstance.py"
${LICENSE_TEXT}
from synchronizers.new_base.policy import Policy
class ${UC_SERVICE_NAME}ServiceInstancePolicy(Policy):
"""
${UC_SERVICE_NAME}ServiceInstancePolicy
Implements model policy for ${UC_SERVICE_NAME}Instance
"""
model_name = "${UC_SERVICE_NAME}ServiceInstance"
def handle_create(self, si):
self.logger.debug(
"MODEL_POLICY: enter handle_create for ${UC_SERVICE_NAME}ServiceInstance %s" %
si.id)
self.handle_update(si)
# TODO: Implement creation policy, if it differs from update policy
def handle_update(self, si):
self.logger.debug(
"MODEL_POLICY: enter handle_update for ${UC_SERVICE_NAME}ServiceInstance %s, valid=%s" %
(si.id, si.valid))
if (si.backend_code != 1):
raise Exception(
"MODEL_POLICY: ${UC_SERVICE_NAME}ServiceInstance %s has not been synced yet" %
si.id)
# TODO: Implement update policy
def handle_delete(self, si):
self.logger.debug(
"MODEL_POLICY: enter handle_delete for ${UC_SERVICE_NAME}ServiceInstance %s" %
si.id)
# TODO: Implement delete policy
EOF
echo "Creating test for model policy of service instance"
cat << EOF > "xos/synchronizer/model_policies/test_model_policy_${SERVICE_NAME}_serviceinstance.py"
${LICENSE_TEXT}
import os
import sys
import unittest
from mock import Mock
# Hack to load synchronizer framework
test_path = os.path.abspath(os.path.dirname(os.path.realpath(__file__)))
xos_dir = os.path.join(test_path, "../../..")
if not os.path.exists(os.path.join(test_path, "new_base")):
xos_dir = os.path.join(test_path, "../../../../../../orchestration/xos/xos")
services_dir = os.path.join(xos_dir, "../../xos-services")
sys.path.append(xos_dir)
sys.path.append(os.path.join(xos_dir, 'synchronizers', 'new_base'))
# END Hack to load synchronizer framework
# generate model from xproto
def get_models_fn(service_name, xproto_name):
name = os.path.join(service_name, "xos", xproto_name)
if os.path.exists(os.path.join(services_dir, name)):
return name
else:
name = os.path.join(service_name, "xos", "synchronizer", "models", xproto_name)
if os.path.exists(os.path.join(services_dir, name)):
return name
raise Exception("Unable to find service=%s xproto=%s" % (service_name, xproto_name))
# END generate model from xproto
class TestModelPolicy${UC_SERVICE_NAME}ServiceInstance(unittest.TestCase):
def setUp(self):
self.sys_path_save = sys.path
sys.path.append(xos_dir)
sys.path.append(os.path.join(xos_dir, 'synchronizers', 'new_base'))
# Setting up the config module
from xosconfig import Config
config = os.path.join(test_path, "../test_config.yaml")
Config.clear()
Config.init(config, "synchronizer-config-schema.yaml")
# END Setting up the config module
from synchronizers.new_base.mock_modelaccessor_build import build_mock_modelaccessor
build_mock_modelaccessor(xos_dir, services_dir, [
get_models_fn("${SERVICE_NAME}", "${SERVICE_NAME}.xproto"),
])
from synchronizers.new_base.modelaccessor import model_accessor
from model_policy_${SERVICE_NAME}_serviceinstance import ${UC_SERVICE_NAME}ServiceInstancePolicy
# import all class names to globals
for (k, v) in model_accessor.all_model_classes.items():
globals()[k] = v
# Some of the functions we call have side-effects, reset the world.
model_accessor.reset_all_object_stores()
self.policy = ${UC_SERVICE_NAME}ServiceInstancePolicy()
self.si = Mock()
def tearDown(self):
sys.path = self.sys_path_save
self.si = None
def test_not_synced(self):
self.si.valid = "awaiting"
self.si.backend_code = 0
with self.assertRaises(Exception) as e:
self.policy.handle_update(self.si)
self.assertIn("has not been synced yet", e.exception.message)
def test_skip_update(self):
self.si.valid = "awaiting"
self.si.backend_code = 1
self.policy.handle_update(self.si)
if __name__ == '__main__':
unittest.main()
EOF
echo "Creating helm chart for service"
cat << EOF > "helm-charts/${SERVICE_NAME}/Chart.yaml"
---
${LICENSE_TEXT}
name: ${SERVICE_NAME}
version: ${SERVICE_VERSION}
EOF
cat << EOF > "helm-charts/${SERVICE_NAME}/values.yaml"
---
${LICENSE_TEXT}
nameOverride: ""
fullnameOverride: ""
${SERVICE_NAME}_synchronizerImage: "xosproject/${SERVICE_NAME}-synchronizer:{{ .Chart.Version }}"
imagePullPolicy: 'IfNotPresent'
xosAdminUser: "admin@opencord.org"
xosAdminPassword: "letmein"
affinity: {}
nodeSelector: {}
replicaCount: 1
resources: {}
tolerations: []
EOF
cat << EOF > "helm-charts/${SERVICE_NAME}/templates/_helpers.tpl"
{{/* vim: set filetype=mustache: */}}
${TPL_LICENSE_TEXT}
{{/*
Expand the name of the chart.
*/}}
{{- define "${SERVICE_NAME}.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "${SERVICE_NAME}.fullname" -}}
{{- if .Values.fullnameOverride -}}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- \$name := default .Chart.Name .Values.nameOverride -}}
{{- if contains \$name .Release.Name -}}
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s" .Release.Name \$name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "${SERVICE_NAME}.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- define "${SERVICE_NAME}.serviceConfig" -}}
name: ${SERVICE_NAME}
accessor:
username: {{ .Values.xosAdminUser | quote }}
password: {{ .Values.xosAdminPassword | quote }}
endpoint: xos-core:50051
required_models:
- ${UC_SERVICE_NAME}Service
- ${UC_SERVICE_NAME}ServiceInstance
dependency_graph: "/opt/xos/synchronizers/${SERVICE_NAME}/model-deps"
model_policies_dir: "/opt/xos/synchronizers/${SERVICE_NAME}/model_policies"
models_dir: "/opt/xos/synchronizers/${SERVICE_NAME}/models"
steps_dir: "/opt/xos/synchronizers/${SERVICE_NAME}/steps"
logging:
version: 1
handlers:
console:
class: logging.StreamHandler
file:
class: logging.handlers.RotatingFileHandler
filename: /var/log/xos.log
maxBytes: 10485760
backupCount: 5
loggers:
'multistructlog':
handlers:
- console
- file
level: DEBUG
{{- end -}}
EOF
cat << EOF > "helm-charts/${SERVICE_NAME}/templates/_tosca.tpl"
{{/* vim: set filetype=mustache: */}}
${TPL_LICENSE_TEXT}
{{- define "${SERVICE_NAME}.serviceTosca" -}}
tosca_definitions_version: tosca_simple_yaml_1_0
description: Set up ${SERVICE_NAME} service
imports:
- custom_types/${SERVICE_NAME}service.yaml
topology_template:
node_templates:
service#${SERVICE_NAME}:
type: tosca.nodes.${UC_SERVICE_NAME}Service
properties:
name: ${SERVICE_NAME}
kind: ${UC_SERVICE_NAME}
{{- end -}}
EOF
cat << EOF > "helm-charts/${SERVICE_NAME}/templates/configmap.yaml"
---
${LICENSE_TEXT}
apiVersion: v1
kind: ConfigMap
metadata:
name: ${SERVICE_NAME}
data:
serviceConfig: |
{{ include "${SERVICE_NAME}.serviceConfig" . | indent 4 }}
EOF
cat << EOF > "helm-charts/${SERVICE_NAME}/templates/deployment.yaml"
---
${LICENSE_TEXT}
apiVersion: apps/v1beta2
kind: Deployment
metadata:
name: {{ template "${SERVICE_NAME}.fullname" . }}
labels:
app: {{ template "${SERVICE_NAME}.name" . }}
chart: {{ template "${SERVICE_NAME}.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app: {{ template "${SERVICE_NAME}.name" . }}
release: {{ .Release.Name }}
template:
metadata:
labels:
app: {{ template "${SERVICE_NAME}.name" . }}
release: {{ .Release.Name }}
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
spec:
containers:
- name: {{ .Chart.Name }}
image: {{ tpl .Values.${SERVICE_NAME}_synchronizerImage . | quote }}
imagePullPolicy: {{ .Values.imagePullPolicy }}
resources:
{{ toYaml .Values.resources | indent 12 }}
volumeMounts:
- name: ${SERVICE_NAME}-config
mountPath: /opt/xos/synchronizers/${SERVICE_NAME}/config.yaml
subPath: config.yaml
- name: certchain-volume
mountPath: /usr/local/share/ca-certificates/local_certs.crt
subPath: config/ca_cert_chain.pem
volumes:
- name: ${SERVICE_NAME}-config
configMap:
name: ${SERVICE_NAME}
items:
- key: serviceConfig
path: config.yaml
- name: certchain-volume
configMap:
name: ca-certificates
items:
- key: chain
path: config/ca_cert_chain.pem
{{- with .Values.nodeSelector }}
nodeSelector:
{{ toYaml . | indent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{ toYaml . | indent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{ toYaml . | indent 8 }}
{{- end }}
EOF
echo 'Done! Check the newly created service repo with service_lint.sh'