kuryr-kubernetes/devstack/lib/kuryr_kubernetes
Michał Dulko 1c1d1d463a DevStack: Support dual stack
This commit makes sure DevStack plugin is able to run with dual stack
and create 2 subnetpools, 2 service subnets and 2 pod subnets. The K8s
is also configured with that.

Implements: blueprint dual-stack

Change-Id: I9c53bc4dd3529a48f5ba1ab77268d6a984a84808
2021-03-03 17:16:49 +01:00

1410 lines
44 KiB
Bash

#!/bin/bash
#
# lib/kuryr
# Utilities for kuryr-kubernetes devstack
# bind_for_kubelet
# Description: Creates an OVS internal port so that baremetal kubelet will be
# able to make both liveness and readiness http/tcp probes.
# Params:
# project - Id or name of the project used for kuryr devstack
# port - Port to open for K8s API, relevant only for OpenStack infra
# Dependencies:
# (none)
function ovs_bind_for_kubelet() {
local port_id
local port_mac
local fixed_ips
local port_ips
local port_subnets
local prefix
local project_id
local port_number
local security_group
local ifname
local service_subnet_cidr
local pod_subnet_gw
local cidrs
project_id="$1"
port_number="$2"
security_group=$(openstack security group list \
--project "$project_id" -c ID -c Name -f value | \
awk '{if ($2=="default") print $1}')
port_id=$(openstack port create \
--device-owner compute:kuryr \
--project "$project_id" \
--security-group "$security_group" \
--security-group service_pod_access \
--host "${HOSTNAME}" \
--network "${KURYR_NEUTRON_DEFAULT_POD_NET}" \
-f value -c id \
kubelet-"${HOSTNAME}")
ifname="kubelet${port_id}"
ifname="${ifname:0:14}"
port_mac=$(openstack port show "$port_id" -c mac_address -f value)
fixed_ips=$(openstack port show "$port_id" -f value -c fixed_ips)
port_ips=($(python3 -c "print(' '.join([x['ip_address'] for x in ${fixed_ips}]))"))
port_subnets=($(python3 -c "print(' '.join([x['subnet_id'] for x in ${fixed_ips}]))"))
sudo ovs-vsctl -- --may-exist add-port $OVS_BRIDGE "$ifname" \
-- set Interface "$ifname" type=internal \
-- set Interface "$ifname" external-ids:iface-status=active \
-- set Interface "$ifname" external-ids:attached-mac="$port_mac" \
-- set Interface "$ifname" external-ids:iface-id="$port_id"
sudo ip link set dev "$ifname" address "$port_mac"
sudo ip link set dev "$ifname" up
for i in "${!port_ips[@]}"; do
prefix=$(openstack subnet show "${port_subnets[$i]}" \
-c cidr -f value | \
cut -f2 -d/)
sudo ip addr add "${port_ips[$i]}/${prefix}" dev "$ifname"
done
# TODO(dulek): This hack is for compatibility with multinode job, we might
# want to do it better one day and actually support dual stack
# and NP here.
if [[ -z ${KURYR_SERVICE_SUBNETS_IDS} ]]; then
KURYR_SERVICE_SUBNETS_IDS=(${KURYR_NEUTRON_DEFAULT_SERVICE_SUBNET}-IPv4)
KURYR_POD_SUBNETS_IDS=(${KURYR_NEUTRON_DEFAULT_POD_SUBNET}-IPv4)
fi
for i in "${!KURYR_SERVICE_SUBNETS_IDS[@]}"; do
pod_subnet_gw=$(openstack subnet show "${KURYR_POD_SUBNETS_IDS[$i]}" \
-c gateway_ip -f value)
if [[ "$KURYR_SUBNET_DRIVER" == "namespace" ]]; then
cidrs=$(openstack subnet pool show "${KURYR_SUBNETPOOLS_IDS[$i]}" -c prefixes -f value)
subnetpool_cidr=$(python3 -c "print(${cidrs}[0])")
sudo ip route add "$subnetpool_cidr" via "$pod_subnet_gw" dev "$ifname"
else
service_subnet_cidr=$(openstack --os-cloud devstack-admin \
--os-region "$REGION_NAME" \
subnet show "${KURYR_SERVICE_SUBNETS_IDS[$i]}" \
-c cidr -f value)
sudo ip route add "$service_subnet_cidr" via "$pod_subnet_gw" dev "$ifname"
fi
done
if [ -n "$port_number" ]; then
# if openstack-INPUT chain doesn't exist we create it in INPUT (for
# local development envs since openstack-INPUT is usually only in gates)
if [[ "$KURYR_IPV6" == "False" || "$KURYR_DUAL_STACK" == "True" ]]; then
sudo iptables -I openstack-INPUT 1 \
-p tcp -s 0.0.0.0/0 -d 0.0.0.0/0 --dport $port_number -j ACCEPT || \
sudo iptables -I INPUT 1 \
-p tcp -m conntrack --ctstate NEW \
-m tcp --dport "$port_number" \
-m comment --comment "kuryr-devstack: Access to OpenShift API" -j ACCEPT
fi
if [[ "$KURYR_IPV6" == "True" || "$KURYR_DUAL_STACK" == "True" ]]; then
sudo ip6tables -I openstack-INPUT 1 \
-p tcp -s ::/0 -d ::/0 --dport $port_number -j ACCEPT || \
sudo ip6tables -I INPUT 1 \
-p tcp -m conntrack --ctstate NEW \
-m tcp --dport "$port_number" \
-m comment --comment "kuryr-devstack: Access to OpenShift API" -j ACCEPT
fi
fi
}
# get_container
# Description: Pulls a container from Dockerhub
# Params:
# image_name - the name of the image in docker hub
# version - The version of the image to pull. Defaults to 'latest'
function get_container {
local image
local image_name
local version
image_name="$1"
version="${2:-latest}"
if [ "$image_name" == "" ]; then
return 0
fi
image="${image_name}:${version}"
if [ -z "$(container_runtime images -q "$image")" ]; then
container_runtime pull "$image"
fi
}
# run_container
# Description: Runs a container and attaches devstack's logging to it
# Params:
# name - Name of the container to run
# args - arguments to run the container with
function run_container {
# Runs a detached container and uses devstack's run process to monitor
# its logs
local name
name="$1"
shift
args="$@"
container_runtime create --name $name $args
if [[ ${CONTAINER_ENGINE} == 'crio' ]]; then
run_process "$name" "$(which podman) start --attach $name" root root
else
run_process "$name" "$(which docker) start --attach $name"
fi
}
# stop_container
# Description: stops a container and its devstack logging
# Params:
# name - Name of the container to stop
function stop_container {
local name
name="$1"
container_runtime kill "$name"
container_runtime rm "$name"
stop_process "$name"
}
# _allocation_range
# Description: Writes out tab separated usable ip range for a CIDR
# Params:
# cidr - The cidr to get the range for
# gateway_position - Whether to reserve at 'beginning' or at 'end'
function _allocation_range {
python3 - <<EOF "$@"
import sys
from netaddr import IPNetwork
n = IPNetwork(str(sys.argv[1]))
gateway_position = sys.argv[2]
if gateway_position == 'beginning':
beg_offset = 2
end_offset = 2
elif gateway_position == 'end':
beg_offset = 1
end_offset = 3
else:
raise ValueError('Disallowed gateway position %s' % gateway_position)
print("%s\\t%s" % (n[beg_offset], n[-end_offset]))
EOF
}
# create_k8s_icmp_sg_rules
# Description: Creates icmp sg rules for Kuryr-Kubernetes pods
# Params:
# sg_id - Kuryr's security group id
# direction - egress or ingress direction
function create_k8s_icmp_sg_rules {
local sg_id=$1
local direction="$2"
local project_id
project_id=$(get_or_create_project \
"$KURYR_NEUTRON_DEFAULT_PROJECT" default)
for ethertype in ${KURYR_ETHERTYPES[@]}; do
icmp_sg_rules=$(openstack --os-cloud devstack-admin \
--os-region "$REGION_NAME" \
security group rule create \
--project "$project_id" \
--protocol icmp \
--ethertype "$ethertype" \
--"$direction" "$sg_id")
done
die_if_not_set $LINENO icmp_sg_rules \
"Failure creating icmp sg ${direction} rule for ${sg_id}"
}
# create_k8s_subnet
# Description: Creates a network and subnet for Kuryr-Kubernetes usage
# Params:
# project_id - Kuryr's project uuid
# net_id - ID of the network where to create subnet in
# subnet_name - Name of the subnet to create
# subnetpool_id - uuid of the subnet pool to use
# router - name of the router to plug the subnet to
# split_allocation - Whether to allocate on all the subnet or only the
# latter half
# ip_version - IPv4 or IPv6
function create_k8s_subnet {
local project_id=$1
local net_id="$2"
local subnet_name="$3"
local subnetpool_id="$4"
local router="$5"
local subnet_params="--project $project_id "
local subnet_cidr
local split_allocation
split_allocation="${6:-False}"
local ip_version="${7:-IPv4}"
if [ "$ip_version" == "IPv4" ]; then
subnet_params+="--ip-version 4 "
else
# NOTE(dulek): K8s API won't accept subnets bigger than 20 bits.
# And 20 will totally be fine for us.
subnet_params+="--ip-version 6 --prefix-length 108 "
fi
subnet_params+="--no-dhcp --gateway none "
subnet_params+="--subnet-pool $subnetpool_id "
subnet_params+="--network $net_id $subnet_name"
local subnet_id
subnet_id=$(openstack --os-cloud devstack-admin \
--os-region "$REGION_NAME" \
subnet create $subnet_params \
--project "$project_id" \
-c id -f value)
die_if_not_set $LINENO subnet_id \
"Failure creating K8s ${subnet_name} IPv4 subnet for ${project_id}"
subnet_cidr=$(openstack --os-cloud devstack-admin \
--os-region "$REGION_NAME" \
subnet show "$subnet_id" \
-c cidr -f value)
die_if_not_set $LINENO subnet_cidr \
"Failure getting K8s ${subnet_name} IPv4 subnet for $project_id"
# Since K8s has its own IPAM for services and allocates the first IP from
# service subnet CIDR to Kubernetes apiserver, we'll always put the router
# interface at the end of the range.
local router_ip
local allocation_start
local allocation_end
local allocation_subnet
router_ip=$(_cidr_range "$subnet_cidr" | cut -f3)
if [[ "$split_allocation" == "True" ]]; then
allocation_subnet=$(split_subnet "$subnet_cidr" | cut -f2)
allocation_start=$(_allocation_range "$allocation_subnet" end | cut -f1)
allocation_end=$(_allocation_range "$allocation_subnet" end | cut -f2)
else
allocation_start=$(_allocation_range "$subnet_cidr" end | cut -f1)
allocation_end=$(_allocation_range "$subnet_cidr" end | cut -f2)
fi
die_if_not_set $LINENO router_ip \
"Failed to determine K8s ${subnet_name} subnet router IP"
openstack --os-cloud devstack-admin \
--os-region "$REGION_NAME" subnet set \
--gateway "$router_ip" --no-allocation-pool "$subnet_id" \
|| die $LINENO "Failed to update K8s ${subnet_name} subnet"
# Set a new allocation pool for the subnet so ports can be created again
openstack --os-cloud devstack-admin \
--os-region "$REGION_NAME" subnet set \
--allocation-pool "start=${allocation_start},end=${allocation_end}" \
"$subnet_id" || die $LINENO "Failed to update K8s ${subnet_name} subnet"
openstack --os-cloud devstack-admin \
--os-region "$REGION_NAME" \
router add subnet "$router" "$subnet_id" \
|| die $LINENO \
"Failed to enable routing for K8s ${subnet_name} subnet"
echo $subnet_id
}
# create_k8s_fake_service
# Description: Creates an endpoint-less kubernetes service to keep Kubernetes
# API server from allocating this IP for another service
function create_k8s_fake_service {
local fake_svc_name
local fake_svc_ip
fake_svc_name="$1"
fake_svc_ip="$2"
existing_svc_ip=$(/usr/local/bin/kubectl get svc --namespace kube-system -o jsonpath='{.items[?(@.metadata.name=='"\"${fake_svc_name}\""')].spec.clusterIP}')
if [[ "$existing_svc_ip" == "" ]]; then
# Create fake service so the clusterIP can't be reassigned
cat <<EOF | /usr/local/bin/kubectl create -f -
kind: Service
apiVersion: v1
metadata:
name: "${fake_svc_name}"
namespace: kube-system
spec:
type: ClusterIP
clusterIP: "${fake_svc_ip}"
ports:
- protocol: TCP
port: 80
EOF
fi
}
# build_kuryr_containers
# Description: Generates a Kuryr controller and Kuryr CNI docker images in
# the local docker registry as kuryr/controller:latest and
# kuryr/cni:latest respectively
function build_kuryr_containers() {
local build_args
local build_dir
build_dir="${DEST}/kuryr-kubernetes"
pushd "$build_dir"
KURYR_CONTAINERS_USE_LOWER_CONSTRAINTS=$(trueorfalse False KURYR_CONTAINERS_USE_LOWER_CONSTRAINTS)
if [[ "$KURYR_CONTAINERS_USE_LOWER_CONSTRAINTS" == "True" ]]; then
build_args="--build-arg UPPER_CONSTRAINTS_FILE=/opt/kuryr-kubernetes/lower-constraints.txt"
fi
# Build images
# FIXME(dulek): Until https://github.com/containers/buildah/issues/1206 is
# resolved instead of podman we need to use buildah directly,
# hence this awful if clause.
if [[ ${CONTAINER_ENGINE} == 'crio' ]]; then
sudo buildah bud -t docker.io/kuryr/controller -f controller.Dockerfile .
sudo buildah bud -t docker.io/kuryr/cni -f cni.Dockerfile .
else
container_runtime build -t kuryr/controller -f controller.Dockerfile ${build_args} .
container_runtime build -t kuryr/cni -f cni.Dockerfile ${build_args} .
fi
popd
}
function indent() {
sed 's/^/ /';
}
function generate_kuryr_configmap() {
local output_dir
local conf_path
output_dir=$1
conf_path=${2:-""}
mkdir -p "$output_dir"
rm -f ${output_dir}/config_map.yml
cat >> "${output_dir}/config_map.yml" << EOF
apiVersion: v1
kind: ConfigMap
metadata:
name: kuryr-config
namespace: kube-system
data:
kuryr.conf: |
EOF
cat $conf_path | indent >> "${output_dir}/config_map.yml"
}
function generate_kuryr_certificates_secret() {
local output_dir
local certs_bundle_path
output_dir=$1
certs_bundle_path=${2:-""}
mkdir -p "$output_dir"
rm -f ${output_dir}/certificates_secret.yml
CA_CERT=\"\" # It's a "" string that will be inserted into yaml file.
if [ $certs_bundle_path -a -f $certs_bundle_path ]; then
CA_CERT=$(base64 -w0 < "$certs_bundle_path")
fi
cat >> "${output_dir}/certificates_secret.yml" << EOF
apiVersion: v1
kind: Secret
metadata:
name: kuryr-certificates
namespace: kube-system
type: Opaque
data:
kuryr-ca-bundle.crt: $CA_CERT
EOF
}
# Generates kuryr-controller service account and kuryr-cni service account.
function generate_kuryr_service_account() {
output_dir=$1
mkdir -p "$output_dir"
rm -f ${output_dir}/controller_service_account.yml
rm -f ${output_dir}/cni_service_account.yml
cat >> "${output_dir}/controller_service_account.yml" << EOF
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: kuryr-controller
namespace: kube-system
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: kuryr-controller
rules:
- apiGroups:
- ""
verbs: ["*"]
resources:
- endpoints
- pods
- nodes
- services
- services/status
- namespaces
- apiGroups:
- openstack.org
verbs: ["*"]
resources:
- kuryrnets
- kuryrnetworks
- kuryrnetpolicies
- kuryrnetworkpolicies
- kuryrloadbalancers
- kuryrports
- apiGroups: ["networking.k8s.io"]
resources:
- networkpolicies
verbs:
- get
- list
- watch
- update
- patch
- apiGroups: ["k8s.cni.cncf.io"]
resources:
- network-attachment-definitions
verbs:
- get
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: kuryr-controller-global
subjects:
- kind: ServiceAccount
name: kuryr-controller
namespace: kube-system
roleRef:
kind: ClusterRole
name: kuryr-controller
apiGroup: rbac.authorization.k8s.io
EOF
cat >> "${output_dir}/cni_service_account.yml" << EOF
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: kuryr-cni
namespace: kube-system
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: kuryr-cni
rules:
- apiGroups:
- ""
verbs: ["*"]
resources:
- pods
- nodes
- apiGroups:
- openstack.org
verbs: ["*"]
resources:
- kuryrports
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: kuryr-cni-global
subjects:
- kind: ServiceAccount
name: kuryr-cni
namespace: kube-system
roleRef:
kind: ClusterRole
name: kuryr-cni
apiGroup: rbac.authorization.k8s.io
EOF
}
function generate_controller_deployment() {
output_dir=$1
health_server_port=$2
controller_ha=$3
mkdir -p "$output_dir"
rm -f ${output_dir}/controller_deployment.yml
cat >> "${output_dir}/controller_deployment.yml" << EOF
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
name: kuryr-controller
name: kuryr-controller
namespace: kube-system
spec:
replicas: ${KURYR_CONTROLLER_REPLICAS:-1}
selector:
matchLabels:
name: kuryr-controller
EOF
# When running without HA we should make sure that we won't have more than
# one kuryr-controller pod in the deployment.
if [ "$controller_ha" == "False" ]; then
cat >> "${output_dir}/controller_deployment.yml" << EOF
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 0
maxUnavailable: 1
EOF
fi
cat >> "${output_dir}/controller_deployment.yml" << EOF
template:
metadata:
labels:
name: kuryr-controller
name: kuryr-controller
spec:
serviceAccountName: kuryr-controller
automountServiceAccountToken: true
hostNetwork: true
containers:
EOF
if [ "$controller_ha" == "True" ]; then
cat >> "${output_dir}/controller_deployment.yml" << EOF
- image: gcr.io/google_containers/leader-elector:0.5
name: leader-elector
args:
- "--election=kuryr-controller"
- "--http=0.0.0.0:${KURYR_CONTROLLER_HA_PORT:-16401}"
- "--election-namespace=kube-system"
- "--ttl=5s"
ports:
- containerPort: ${KURYR_CONTROLLER_HA_PORT:-16401}
protocol: TCP
EOF
fi
cat >> "${output_dir}/controller_deployment.yml" << EOF
- image: kuryr/controller:latest
imagePullPolicy: Never
name: controller
terminationMessagePath: "/dev/termination-log"
volumeMounts:
- name: config-volume
mountPath: "/etc/kuryr"
- name: certificates-volume
mountPath: "/etc/ssl/certs"
readOnly: true
readinessProbe:
httpGet:
path: /ready
port: ${health_server_port}
scheme: HTTP
timeoutSeconds: 5
livenessProbe:
httpGet:
path: /alive
port: ${health_server_port}
initialDelaySeconds: 15
EOF
cat >> "${output_dir}/controller_deployment.yml" << EOF
volumes:
- name: config-volume
configMap:
name: kuryr-config
- name: certificates-volume
secret:
secretName: kuryr-certificates
restartPolicy: Always
tolerations:
- key: "node-role.kubernetes.io/master"
operator: "Exists"
effect: "NoSchedule"
- key: "node.kubernetes.io/not-ready"
operator: "Exists"
effect: "NoSchedule"
EOF
}
function generate_cni_daemon_set() {
output_dir=$1
cni_health_server_port=$2
cni_bin_dir=${3:-/opt/cni/bin}
cni_conf_dir=${4:-/etc/cni/net.d}
mkdir -p "$output_dir"
rm -f ${output_dir}/cni_ds.yml
cat >> "${output_dir}/cni_ds.yml" << EOF
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: kuryr-cni-ds
namespace: kube-system
labels:
tier: node
app: kuryr-cni
spec:
selector:
matchLabels:
app: kuryr-cni
template:
metadata:
labels:
tier: node
app: kuryr-cni
spec:
hostNetwork: true
tolerations:
- key: node-role.kubernetes.io/master
operator: Exists
effect: NoSchedule
- key: "node.kubernetes.io/not-ready"
operator: "Exists"
effect: "NoSchedule"
serviceAccountName: kuryr-cni
containers:
- name: kuryr-cni
image: kuryr/cni:latest
imagePullPolicy: Never
command: [ "cni_ds_init" ]
env:
- name: KUBERNETES_NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
- name: KURYR_CNI_POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
securityContext:
privileged: true
volumeMounts:
- name: bin
mountPath: /opt/cni/bin
- name: net-conf
mountPath: /etc/cni/net.d
- name: config-volume
mountPath: /etc/kuryr
- name: proc
mountPath: /host_proc
- name: var-pci
mountPath: /var/pci_address
EOF
if [[ -n "$VAR_RUN_PATH" ]]; then
cat >> "${output_dir}/cni_ds.yml" << EOF
- name: openvswitch
mountPath: /var/run
EOF
fi
cat >> "${output_dir}/cni_ds.yml" << EOF
readinessProbe:
httpGet:
path: /ready
port: ${cni_health_server_port}
scheme: HTTP
initialDelaySeconds: 60
timeoutSeconds: 10
livenessProbe:
httpGet:
path: /alive
port: ${cni_health_server_port}
initialDelaySeconds: 60
volumes:
- name: bin
hostPath:
path: ${cni_bin_dir}
- name: net-conf
hostPath:
path: ${cni_conf_dir}
- name: config-volume
configMap:
name: kuryr-config
- name: proc
hostPath:
path: /proc
- name: var-pci
hostPath:
path: /var/pci_address
EOF
if [[ -n "$VAR_RUN_PATH" ]]; then
cat >> "${output_dir}/cni_ds.yml" << EOF
- name: openvswitch
hostPath:
path: ${VAR_RUN_PATH}
EOF
fi
}
# install_openshift_binary
# Description: Fetches the configured binary release of OpenShift and
# installs it in the system
function install_openshift_binary {
mkdir -p "$OPENSHIFT_BIN"
curl -L ${OPENSHIFT_BINARY_BASE_URL}/${OPENSHIFT_BINARY_VERSION}/CHECKSUM --silent | \
awk -v "ver=${OPENSHIFT_BINARY_VERSION}" \
-v "dest=${OPENSHIFT_BIN}/openshift.tar.gz" \
-v "baseurl=${OPENSHIFT_BINARY_BASE_URL}" \
'/server/ {system("curl -L " baseurl "/" ver "/" $2 " --retry 2 -o " dest)}'
tar xzvf "${OPENSHIFT_BIN}/openshift.tar.gz" --strip 1 -C "$OPENSHIFT_BIN"
# Make openshift run from its untarred directory
cat << EOF | sudo tee /usr/local/bin/openshift
#!/bin/bash
cd ${OPENSHIFT_BIN}
exec ./openshift "\$@"
EOF
sudo chmod a+x /usr/local/bin/openshift
# For releases >= 3.11 we'll need hyperkube as well
cat << EOF | sudo tee /usr/local/bin/hyperkube
#!/bin/bash
cd ${OPENSHIFT_BIN}
exec ./hyperkube "\$@"
EOF
sudo chmod a+x /usr/local/bin/hyperkube
# Make oc easily available
cat << EOF | sudo tee /usr/local/bin/oc
#!/bin/bash
CURL_CA_BUNDLE=${OPENSHIFT_DATA_DIR}/master/ca.crt \
KUBECONFIG=${OPENSHIFT_DATA_DIR}/master/admin.kubeconfig \
${OPENSHIFT_BIN}/oc "\$@"
EOF
sudo chmod a+x /usr/local/bin/oc
# Make kubectl easily available
cat << EOF | sudo tee /usr/local/bin/kubectl
#!/bin/bash
CURL_CA_BUNDLE=${OPENSHIFT_DATA_DIR}/master/ca.crt \
KUBECONFIG=${OPENSHIFT_DATA_DIR}/master/admin.kubeconfig \
${OPENSHIFT_BIN}/kubectl "\$@"
EOF
sudo chmod a+x /usr/local/bin/kubectl
}
# run_openshift_master
# Description: Starts the openshift master
function run_openshift_master {
local cmd
local pod_subnet_cidr
local service_subnet_cidr
local portal_net
sudo install -d -o "$STACK_USER" "$OPENSHIFT_DATA_DIR"
pod_subnet_cidr=$(openstack --os-cloud devstack-admin \
--os-region "$REGION_NAME" \
subnet show "$KURYR_NEUTRON_DEFAULT_POD_SUBNET" \
-c cidr -f value)
service_subnet_cidr=$(openstack --os-cloud devstack-admin \
--os-region "$REGION_NAME" \
subnet show "$KURYR_NEUTRON_DEFAULT_SERVICE_SUBNET" \
-c cidr -f value)
if is_service_enabled octavia; then
portal_net=$(split_subnet "$service_subnet_cidr" | cut -f1)
else
portal_net="$service_subnet_cidr"
fi
# Generate master config
"${OPENSHIFT_BIN}/openshift" start master \
"--etcd=http://${SERVICE_HOST}:${ETCD_PORT}" \
"--network-cidr=${pod_subnet_cidr}" \
"--portal-net=${portal_net}" \
"--listen=0.0.0.0:${OPENSHIFT_API_PORT}" \
"--master=${OPENSHIFT_API_URL}" \
"--write-config=${OPENSHIFT_DATA_DIR}/master"
# Enable externalIPs
sed -i 's/externalIPNetworkCIDRs: null/externalIPNetworkCIDRs: ["0.0.0.0\/0"]/' "${OPENSHIFT_DATA_DIR}/master/master-config.yaml"
# Reconfigure Kuryr-Kubernetes to use the certs generated
iniset "$KURYR_CONFIG" kubernetes ssl_client_crt_file "${OPENSHIFT_DATA_DIR}/master/admin.crt"
iniset "$KURYR_CONFIG" kubernetes ssl_client_key_file "${OPENSHIFT_DATA_DIR}/master/admin.key"
iniset "$KURYR_CONFIG" kubernetes ssl_ca_crt_file "${OPENSHIFT_DATA_DIR}/master/ca.crt"
sudo chown "${STACK_USER}:${STACK_USER}" -R "$OPENSHIFT_DATA_DIR"
# Generate kubelet kubeconfig
"${OPENSHIFT_BIN}/oc" adm create-kubeconfig \
"--client-key=${OPENSHIFT_DATA_DIR}/master/master.kubelet-client.key" \
"--client-certificate=${OPENSHIFT_DATA_DIR}/master/master.kubelet-client.crt" \
"--certificate-authority=${OPENSHIFT_DATA_DIR}/master/ca.crt" \
"--master=${OPENSHIFT_API_URL}" \
"--kubeconfig=${OPENSHIFT_DATA_DIR}/master/master.kubelet-client.kubeconfig"
cmd="/usr/local/bin/openshift start master \
--config=${OPENSHIFT_DATA_DIR}/master/master-config.yaml"
wait_for "etcd" "http://${SERVICE_HOST}:${ETCD_PORT}/v2/machines"
run_process openshift-master "$cmd" root root
}
# make_admin_cluster_admin
# Description: Gives the system:admin permissions over the cluster
function make_admin_cluster_admin {
wait_for "OpenShift API Server" "$OPENSHIFT_API_URL" \
"${OPENSHIFT_DATA_DIR}/master/ca.crt"
/usr/local/bin/oc adm policy add-cluster-role-to-user cluster-admin admin \
"--config=${OPENSHIFT_DATA_DIR}/master/openshift-master.kubeconfig"
/usr/local/bin/oc adm policy add-cluster-role-to-user cluster-admin system:openshift-node-admin \
"--config=${OPENSHIFT_DATA_DIR}/master/openshift-master.kubeconfig"
}
# run_openshift_node
# Description: Starts the openshift node
function run_openshift_node {
local command
#install required CNI loopback driver
sudo mkdir -p "$CNI_BIN_DIR"
curl -L "$OPENSHIFT_CNI_BINARY_URL" | sudo tar -C "$CNI_BIN_DIR" -xzvf - ./loopback
# Since 3.11 we should run upstream kubelet through hyperkube.
declare -r min_no_node_ver="v3.11.0"
if [[ "$min_no_node_ver" == "$(echo -e "${OPENSHIFT_BINARY_VERSION}\n${min_no_node_ver}" | sort -V | head -n 1)" ]]; then
# generate kubelet configuration and certs
local name
name=`hostname`
oc adm create-node-config --node-dir ${OPENSHIFT_DATA_DIR}/node \
--node ${name} \
--hostnames ${name} \
--certificate-authority ${OPENSHIFT_DATA_DIR}/master/ca.crt \
--signer-cert ${OPENSHIFT_DATA_DIR}/master/ca.crt \
--signer-key=${OPENSHIFT_DATA_DIR}/master/ca.key \
--signer-serial ${OPENSHIFT_DATA_DIR}/master/ca.serial.txt \
--node-client-certificate-authority=${OPENSHIFT_DATA_DIR}/master/ca.crt
command="/usr/local/bin/hyperkube kubelet \
--network-plugin=cni \
--address=0.0.0.0 \
--port=10250 \
--cgroup-driver $(docker info -f '{{.CgroupDriver}}') \
--fail-swap-on=false \
--allow-privileged=true \
--v=2 \
--tls-cert-file=${OPENSHIFT_DATA_DIR}/node/server.crt \
--tls-private-key-file=${OPENSHIFT_DATA_DIR}/node/server.key"
else
command="/usr/local/bin/openshift start node \
--enable=kubelet,plugins \
--network-plugin=cni \
--listen=https://0.0.0.0:8442"
fi
command+=" --kubeconfig=${OPENSHIFT_DATA_DIR}/master/master.kubelet-client.kubeconfig"
# Link master config necessary for bootstrapping
# TODO: This needs to be generated so we don't depend on it on multinode
mkdir -p "${OPENSHIFT_BIN}/openshift.local.config"
ln -fs "${OPENSHIFT_DATA_DIR}/master" "${OPENSHIFT_BIN}/openshift.local.config/master"
mkdir -p "${OPENSHIFT_DATA_DIR}/node"
ln -fs "${OPENSHIFT_DATA_DIR}/node" "${OPENSHIFT_BIN}/openshift.local.config/node"
# Link stack CNI to location expected by openshift node
sudo mkdir -p /etc/cni
sudo rm -fr /etc/cni/net.d
sudo rm -fr /opt/cni/bin
sudo ln -fs "${CNI_CONF_DIR}" /etc/cni/net.d
sudo mkdir -p /opt/cni
sudo ln -fs "${CNI_BIN_DIR}" /opt/cni/bin
run_process openshift-node "$command" root root
}
# lb_state
# Description: Returns the state of the load balancer
# Params:
# id - Id or name of the loadbalancer the state of which needs to be
# retrieved.
function lb_state {
local lb_id
lb_id="$1"
openstack loadbalancer show "$lb_id" | \
awk '/provisioning_status/ {print $4}'
}
function wait_for_lb {
local lb_name
local curr_time
local time_diff
local start_time
lb_name="$1"
timeout=${2:-$KURYR_WAIT_TIMEOUT}
echo -n "Waiting for LB:$lb_name"
start_time=$(date +%s)
while [[ "$(lb_state "$lb_name")" != "ACTIVE" ]]; do
echo -n "Waiting till LB=$lb_name is ACTIVE."
curr_time=$(date +%s)
time_diff=$((curr_time - start_time))
[[ $time_diff -le $timeout ]] || die "Timed out waiting for $lb_name"
sleep 5
done
}
# create_load_balancer
# Description: Creates an OpenStack Load Balancer with either neutron LBaaS
# or Octavia
# Params:
# lb_name: Name to give to the load balancer.
# lb_vip_subnet: Id or name of the subnet where lb_vip should be
# allocated.
# project_id: Id of the project where the load balancer should be
# allocated.
# lb_vip: Virtual IP to give to the load balancer - optional.
function create_load_balancer {
local lb_name
local lb_vip_subnet
local lb_params
local project_id
lb_name="$1"
lb_vip_subnet="$2"
project_id="$3"
lb_params=" --name $lb_name "
if [ -z "$4" ]; then
echo -n "create_load_balancer LB=$lb_name, lb_vip not provided."
else
lb_params+=" --vip-address $4"
fi
lb_params+=" --project ${project_id} --vip-subnet-id $lb_vip_subnet"
openstack loadbalancer create $lb_params
}
# create_load_balancer_listener
# Description: Creates an OpenStack Load Balancer Listener for the specified
# Load Balancer with either neutron LBaaS or Octavia
# Params:
# name: Name to give to the load balancer listener.
# protocol: Whether it is HTTP, HTTPS, TCP, etc.
# port: The TCP port number to listen to.
# data_timeouts: Octavia's timeouts for client and server inactivity.
# lb: Id or name of the Load Balancer we want to add the Listener to.
# project_id: Id of the project where this listener belongs to.
function create_load_balancer_listener {
local name
local protocol
local port
local lb
local data_timeouts
local max_timeout
local project_id
name="$1"
protocol="$2"
port="$3"
lb="$4"
project_id="$5"
data_timeouts="$6"
max_timeout=1200
# Octavia needs the LB to be active for the listener
wait_for_lb $lb $max_timeout
openstack loadbalancer listener create --name "$name" \
--protocol "$protocol" \
--protocol-port "$port" \
--timeout-client-data "$data_timeouts" \
--timeout-member-data "$data_timeouts" \
"$lb"
}
# create_load_balancer_pool
# Description: Creates an OpenStack Load Balancer Pool for the specified
# Load Balancer listener with either neutron LBaaS or Octavia
# Params:
# name: Name to give to the load balancer listener.
# protocol: Whether it is HTTP, HTTPS, TCP, etc.
# algorithm: Load Balancing algorithm to use.
# listener: Id or name of the Load Balancer Listener we want to add the
# pool to.
# project_id: Id of the project where this pool belongs to.
# lb: Id or name of the Load Balancer we want to add the pool to
# (optional).
function create_load_balancer_pool {
local name
local protocol
local algorithm
local listener
local lb
local project_id
name="$1"
protocol="$2"
algorithm="$3"
listener="$4"
project_id="$5"
lb="$6"
# We must wait for the LB to be active before we can put a Pool for it
wait_for_lb $lb
openstack loadbalancer pool create --name "$name" \
--listener "$listener" \
--protocol "$protocol" \
--lb-algorithm "$algorithm"
}
# create_load_balancer_member
# Description: Creates an OpenStack load balancer pool member
# Params:
# name: Name to give to the load balancer pool member.
# address: Whether it is HTTP, HTTPS, TCP, etc.
# port: Port number the pool member is listening on.
# pool: Id or name of the Load Balancer pool this member belongs to.
# subnet: Id or name of the subnet the member address belongs to.
# lb: Id or name of the load balancer the member belongs to.
# project_id: Id of the project where this pool belongs to.
function create_load_balancer_member {
local name
local address
local port
local pool
local subnet
local lb
local project_id
name="$1"
address="$2"
port="$3"
pool="$4"
subnet="$5"
lb="$6"
project_id="$7"
# We must wait for the pool creation update before we can add members
wait_for_lb $lb
openstack loadbalancer member create --name "$name" \
--address "$address" \
--protocol-port "$port" \
"$pool"
}
# split_subnet
# Description: Splits a subnet in two subnets that constitute its halves
# Params:
# cidr: Subnet CIDR to split
# Returns: tab separated CIDRs of the two halves.
function split_subnet {
# precondition: The passed cidr must be of a prefix <= 30
python3 - <<EOF "$@"
import sys
from netaddr import IPNetwork
n = IPNetwork(str(sys.argv[1]))
first, last = n.subnet(n.prefixlen+1)
print("%s\\t%s" % (first, last))
EOF
}
# get_loadbalancer_attribute
# Description: Get load balancer attribute
# Params:
# lb_name: Load balancer name
# lb_attr: attribute name
function get_loadbalancer_attribute {
local lb_name
local lb_attr
lb_name="$1"
lb_attr="$2"
openstack loadbalancer show "$lb_name" -c "$lb_attr" -f value
}
# openshift_node_set_dns_config
# Description: Configures Openshift node's DNS section atomically
# Params:
# node_conf_path: path_to_node_config
# upstream_dns_ip: IP of the upstream DNS
function openshift_node_set_dns_config {
local openshift_dnsmasq_recursive_resolv
local upstream_dns_ip
openshift_dnsmasq_recursive_resolv="${OPENSHIFT_DATA_DIR}/node/resolv.conf"
upstream_dns_ip="$2"
cat > "$openshift_dnsmasq_recursive_resolv" << EOF
nameserver $upstream_dns_ip
EOF
python3 - <<EOF "$@"
import os
import sys
import tempfile
import traceback
import yaml
if len(sys.argv) < 3:
sys.exit(1)
node_conf_path = sys.argv[1]
conf_dir = os.path.dirname(node_conf_path)
def dns_configure_copy(conf):
new_conf = conf.copy()
# 127.0.0.1 is used by unbound in gates, let's use another localshost addr
new_conf['dnsBindAddress'] = '127.0.0.11:53'
new_conf['dnsDomain'] = 'cluster.local'
new_conf['dnsIP'] = '0.0.0.0'
new_conf['dnsRecursiveResolvConf'] = '${openshift_dnsmasq_recursive_resolv}'
return new_conf
old_config = {}
while True:
tp = tempfile.NamedTemporaryFile(dir=conf_dir, delete=False, mode='w')
try:
with open(node_conf_path) as node_conf:
current_conf = yaml.load(node_conf.read())
if current_conf == old_config:
tp.write(yaml.dump(new_conf, default_flow_style=False))
tp.flush()
os.fsync(tp.fileno())
tp.close()
os.rename(tp.name, node_conf_path)
break
else:
new_conf = dns_configure_copy(current_conf)
old_config = current_conf
tp.close()
os.unlink(tp.name)
except Exception as e:
traceback.print_exc(file=sys.stdout)
tp.close()
os.unlink(tp.name)
EOF
}
# run_openshift_dnsmasq
# Description: Configures and runs a dnsmasq instance to be run as the node
# DNS server that will choose between openshift's DNS and the
# upstream DNS depending on the domain
# Params:
# upstream_dns_ip: IP of the upstream DNS
function run_openshift_dnsmasq {
local dnmasq_binary
local cmd
local upstream_dns_ip
local openshift_dnsmasq_conf_path
local search_domains
upstream_dns_ip="$1"
openshift_dnsmasq_conf_path="${OPENSHIFT_DATA_DIR}/node/node_dnsmasq.conf"
install_package dnsmasq
cat > "$openshift_dnsmasq_conf_path" << EOF
server=${upstream_dns_ip}
no-resolv
domain-needed
no-negcache
max-cache-ttl=1
# Enable dbus so openshift dns can use it to set cluster.local rules
enable-dbus
dns-forward-max=10000
cache-size=10000
bind-dynamic
# Do not bind to localhost addresses 127.0.0.1/8 (where skydns binds)
except-interface=lo
EOF
#Open port 53 so pods can reach the DNS server
sudo iptables -I INPUT 1 -p udp -m udp --dport 53 -m comment --comment "kuryr-devstack: Access to OpenShift API" -j ACCEPT
dnsmasq_binary="$(command -v dnsmasq)"
cmd="${dnsmasq_binary} -k -C ${openshift_dnsmasq_conf_path}"
run_process openshift-dnsmasq "$cmd" root root
sudo cp /etc/resolv.conf /etc/resolv.conf.orig
search_domains=$(awk '/search/ {for (i=2; i<NF; i++) printf $i " "; print $NF}' /etc/resolv.conf.orig)
search_domains="cluster.local ${search_domains}"
echo "search ${search_domains}" | sudo tee /etc/resolv.conf.openshift_devstack
echo "options ndots:4" | sudo tee /etc/resolv.conf.openshift_devstack
echo "nameserver ${HOST_IP}" | sudo tee --append /etc/resolv.conf.openshift_devstack
grep "nameserver" /etc/resolv.conf.orig | sudo tee --append /etc/resolv.conf.openshift_devstack
sudo mv /etc/resolv.conf.openshift_devstack /etc/resolv.conf
}
function reinstate_old_dns_config {
sudo mv /etc/resolv.conf.orig /etc/resolv.conf
}
# run_openshift_dns
# Description: Starts openshift's DNS
function run_openshift_dns {
local command
command="/usr/local/bin/openshift start network \
--enable=dns \
--config=${OPENSHIFT_DATA_DIR}/node/node-config.yaml \
--kubeconfig=${OPENSHIFT_DATA_DIR}/node/node.kubeconfig"
run_process openshift-dns "$command" root root
}
# cleanup_kuryr_devstack_iptables
# Description: Fins all the iptables rules we set and deletes them
function cleanup_kuryr_devstack_iptables {
local chains
chains=( INPUT FORWARD OUTPUT )
for chain in ${chains[@]}; do
sudo iptables -n -L "$chain" -v --line-numbers | \
awk -v chain="$chain" \
'/kuryr-devstack/ {print "sudo iptables -D " chain " " $1}' | \
tac | bash /dev/stdin
done
}
# run_openshift_registry
# Description: Deploys Openshift's registry as a DeploymentConfig
function run_openshift_registry {
local registry_yaml
local registry_ip="$1"
mkdir -p "${OPENSHIFT_DATA_DIR}/registry"
registry_yaml=$(mktemp)
oc adm registry \
--config=${OPENSHIFT_DATA_DIR}/master/admin.kubeconfig \
--service-account=registry \
--mount-host=${OPENSHIFT_DATA_DIR}/registry \
--tls-certificate=${OPENSHIFT_DATA_DIR}/master/registry.crt \
--tls-key=${OPENSHIFT_DATA_DIR}/master/registry.key \
-o yaml > $registry_yaml
python3 - <<EOF "$registry_yaml" "$registry_ip"
import copy
import os
import sys
import tempfile
import traceback
import yaml
if len(sys.argv) < 3:
sys.exit(1)
registry_conf_path = sys.argv[1]
registry_cluster_ip = sys.argv[2]
conf_dir = os.path.dirname(registry_conf_path)
def service_configure_registry_clusterIP(conf):
new_conf = copy.deepcopy(conf)
for object in new_conf['items']:
if object['kind'] == 'Service':
object['spec']['clusterIP'] = registry_cluster_ip
return new_conf
old_conf = {}
while True:
tp = tempfile.NamedTemporaryFile(dir=conf_dir, delete=False, mode='w')
try:
with open(registry_conf_path) as registry_conf:
current_conf = yaml.load(registry_conf.read())
if current_conf == old_conf:
tp.write(yaml.dump(new_conf, default_flow_style=False))
tp.flush()
os.fsync(tp.fileno())
tp.close()
os.rename(tp.name, registry_conf_path)
break
else:
new_conf = service_configure_registry_clusterIP(current_conf)
old_conf = current_conf
tp.close()
os.unlink(tp.name)
except Exception as e:
traceback.print_exc(file=sys.stdout)
tp.close()
os.unlink(tp.name)
EOF
oc adm policy add-scc-to-user privileged -z registry -n default
oc create -f "$registry_yaml"
}
# oc_generate_server_certificates
# Description: Generates and CA signs openshift cert & key for server
# Params:
# - name: filename without extension of the cert and key
# - hostnames: the comma separated hostnames to sign the cert for
function oc_generate_server_certificates {
local name
local cert_hostnames
name="$1"
cert_hostnames="$2"
oc adm ca create-server-cert \
--signer-cert="${OPENSHIFT_DATA_DIR}/master/ca.crt" \
--signer-key="${OPENSHIFT_DATA_DIR}/master/ca.key" \
--signer-serial="${OPENSHIFT_DATA_DIR}/master/ca.serial.txt" \
--hostnames="$cert_hostnames" \
--cert="${OPENSHIFT_DATA_DIR}/master/${name}.crt" \
--key="${OPENSHIFT_DATA_DIR}/master/${name}.key"
}
# docker_install_ca_certs
# Description: Installs registry openshift_ca_certs to docker
# Params:
# - registry_hostnames: the comma separated hostnames to give the CA for
function docker_install_ca_certs {
local registry_hostnames
local destdir
# TODO(dulek): Support for CRI-O.
registry_hostnames=(${1//,/ })
for hostname in ${registry_hostnames[@]}; do
destdir="/etc/docker/certs.d/${hostname}:5000"
sudo install -d -o "$STACK_USER" "$destdir"
sudo install -o "$STACK_USER" "${OPENSHIFT_DATA_DIR}/master/ca.crt" "${destdir}/"
done
}
function _nth_cidr_ip {
local cidr
local position
cidr="$1"
position="$2"
python3 - <<EOF "$cidr" "$position"
import sys
from netaddr import IPAddress, IPNetwork
cmdname, cidr, position = sys.argv
n = IPNetwork(cidr)
print("%s" % IPAddress(n.first + int(position)))
EOF
}
function configure_and_run_registry {
local service_cidr
local registry_ip
local hostnames
# TODO(dulek): Support for CRI-O.
service_cidr=$(openstack --os-cloud devstack-admin \
--os-region "$REGION_NAME" \
subnet show "$KURYR_NEUTRON_DEFAULT_SERVICE_SUBNET" \
-c cidr -f value)
registry_ip=$(_nth_cidr_ip "$service_cidr" 2)
hostnames="docker-registry.default.svc.cluster.local,docker-registry.default.svc,${registry_ip}"
docker_install_ca_certs "$hostnames"
oc_generate_server_certificates registry "$hostnames"
run_openshift_registry "$registry_ip"
}