├── .gitignore ├── .travis.yml ├── AUTHORS ├── Dockerfile ├── Dockerfile-gpu ├── LICENSE ├── README.md ├── bin ├── flux └── test_example ├── deploy ├── docker │ ├── docker_build.sh │ ├── docker_build_gpu.sh │ ├── docker_build_hdfs.sh │ ├── docker_build_ros.sh │ ├── docker_build_ros_gpu.sh │ ├── flux │ │ ├── Dockerfile │ │ ├── Dockerfile-gpu │ │ ├── README.md │ │ ├── jupyterhub_config.py │ │ ├── lib │ │ │ ├── protobuf-java-3.3.0.jar │ │ │ ├── rosbaginputformat.jar │ │ │ ├── rosbaginputformat_2.11-0.9.8.jar │ │ │ └── scala-library-2.11.8.jar │ │ └── spark-ex-kubernetes.sh │ ├── hdfs4k8s │ │ ├── Dockerfile-datanode │ │ ├── Dockerfile-namenode │ │ ├── run-dn.sh │ │ └── run-nn.sh │ └── ros_base │ │ ├── Dockerfile │ │ ├── Dockerfile-gpu │ │ ├── README.txt │ │ └── ros_entrypoint.sh └── kubernetes │ ├── distributed │ ├── README.md │ ├── flux │ ├── flux-init │ ├── hdfs-flux │ │ ├── .helmignore │ │ ├── Chart.yaml │ │ ├── templates │ │ │ ├── NOTES.txt │ │ │ ├── _helpers.tpl │ │ │ ├── dn-ds.yaml │ │ │ ├── hdfs-cm.yaml │ │ │ ├── nn-pod.yaml │ │ │ └── nn-svc.yaml │ │ └── values.yaml │ ├── hdfs-pv │ │ ├── .helmignore │ │ ├── Chart.yaml │ │ ├── templates │ │ │ ├── NOTES.txt │ │ │ ├── _helpers.tpl │ │ │ ├── dn-pv.yaml │ │ │ └── nn-pv.yaml │ │ └── values.yaml │ └── hdfs-pvc │ │ ├── Chart.yaml │ │ ├── templates │ │ ├── _helpers.tpl │ │ ├── dn-pvc.yaml │ │ └── nn-pvc.yaml │ │ └── values.yaml │ ├── flux-ros-hadoop-deployment.yml │ ├── flux-ros-hadoop-gpu-deployment.yml │ ├── flux-ros-hadoop-gpu-service.yml │ └── flux-ros-hadoop-service.yml ├── examples ├── Tutorial.ipynb ├── concept.png ├── data-exploration.ipynb ├── drive-obj-detect.mp4 ├── drive-stats.mp4 ├── header.png ├── lane_detector.py ├── line.py ├── map.png ├── object_detection_model.py ├── object_detector.py ├── rosbag-larger-than-2-GB.ipynb ├── sample-use-cases.ipynb └── utils.py └── images ├── flux_cloud.png ├── flux_overview.png ├── login_notebook.png └── sample_notebook.png /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .ipynb_checkpoints/ 3 | metastore_db/ 4 | target/ 5 | derby.log 6 | *bag 7 | dist/ 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | 3 | language: python 4 | 5 | env: 6 | - CHANGE_MINIKUBE_NONE_USER=true 7 | 8 | services: 9 | - docker 10 | 11 | before_script: 12 | # install minikube 13 | - curl -Lo kubectl https://storage.googleapis.com/kubernetes-release/release/v1.9.0/bin/linux/amd64/kubectl && chmod +x kubectl && sudo mv kubectl /usr/local/bin/ 14 | - curl -Lo minikube https://storage.googleapis.com/minikube/releases/v0.25.2/minikube-linux-amd64 && chmod +x minikube && sudo mv minikube /usr/local/bin/ 15 | - minikube config set WantReportErrorPrompt false 16 | - sudo minikube start --vm-driver=none --kubernetes-version=v1.9.0 17 | - minikube update-context 18 | - JSONPATH='{range .items[*]}{@.metadata.name}:{range @.status.conditions[*]}{@.type}={@.status};{end}{end}'; until kubectl get nodes -o jsonpath="$JSONPATH" 2>&1 | grep -q "Ready=True"; do sleep 1; done 19 | - kubectl cluster-info 20 | - curl https://raw.githubusercontent.com/kubernetes/helm/master/scripts/get > get_helm.sh 21 | - chmod 700 get_helm.sh 22 | - sudo ./get_helm.sh 23 | - helm init 24 | 25 | script: 26 | - ./bin/flux build 27 | # - ./deploy/docker/docker_build_ros_gpu.sh 28 | # - ./deploy/docker/docker_build_gpu.sh 29 | - ./bin/flux start 30 | - sleep 10 # TODO: avoid sleep 31 | - ./bin/flux ps 32 | # TODO: activate once working - (cd examples && find . -name '*.ipynb') | xargs ./bin/test_example 33 | - ./bin/flux stop 34 | - ./bin/flux purge 35 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # This is the official list of Flux authors for copyright purposes. 2 | # This file is distinct from the CONTRIBUTORS files. 3 | # See the latter for an explanation. 4 | 5 | # Names should be added to this file as: 6 | # Name or Organization 7 | # The email address is not required for organizations. 8 | 9 | Jan Wiegelmann git@wiegelmann.net 10 | Adrian Achihăei vasco@consultant.com 11 | Seunghan Han hanseunghan@gmail.com 12 | Karthikeya Sampa Subbarao karthikeya108@gmail.com 13 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM fluxproject/flux 2 | 3 | COPY examples/* /opt/ros_hadoop/latest/doc/ 4 | RUN chmod -R 777 /opt/ros_hadoop/latest/doc 5 | -------------------------------------------------------------------------------- /Dockerfile-gpu: -------------------------------------------------------------------------------- 1 | FROM fluxproject/flux_gpu 2 | 3 | COPY examples/* /opt/ros_hadoop/latest/doc/ 4 | RUN chmod -R 777 /opt/ros_hadoop/latest/doc 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/flux-project/flux.svg?branch=master)](https://travis-ci.org/flux-project/flux) 2 | [![Docker Automation](https://img.shields.io/docker/automated/fluxproject/flux.svg)](https://hub.docker.com/r/fluxproject/flux/) 3 | [![Docker Build Status](https://img.shields.io/docker/build/fluxproject/flux.svg)](https://hub.docker.com/r/fluxproject/flux/) 4 | 5 | # Flux Project 6 | 7 | Autodeploy a complete end-to-end machine/deep learning pipeline on Kubernetes using tools like Spark, TensorFlow, HDFS, etc. - it requires a running Kubernetes (K8s) cluster in the cloud or on-premise. 8 | 9 | Please visit the [website for updates.](http://flux-project.org/ "Flux Project") 10 | 11 | 12 | 13 | ### Prerequisites 14 | Before installing the components make sure you have installed 15 | * [Docker](https://www.docker.com/get-docker) 16 | The edge version of docker community edition is coming with a kubernetes option 17 | * [Kubernetes](https://kubernetes.io/) 18 | * [Helm](https://helm.sh/) 19 | The package manager for Kubernetes. 20 | 21 | ### Deploy on nodes 22 | 23 | `./bin/flux` will check for GPU availability and make use of it if it can find a GPU. 24 | 25 | 1. Build the images 26 | ```bash 27 | ./bin/flux build 28 | ``` 29 | Note that images need to be deployed to your nodes or to your docker registry 30 | 31 | 1. Create the deployment and the service with Kubernetes 32 | ```bash 33 | ./bin/flux start 34 | ``` 35 | 36 | 1. Check that all components are running 37 | ```bash 38 | ./bin/flux ps 39 | ``` 40 | 41 | ### Accessing the sample notebooks: 42 | 43 | ```bash 44 | ./bin/flux notebook 45 | ``` 46 | A browser window opens. You can there login using `flux/flux`. 47 | 48 | 49 | 50 | After Login an ipython notebook playground with examples will open. 51 | 52 | 53 | 54 | ### Cloud deployment 55 | 56 | 57 | -------------------------------------------------------------------------------- /bin/flux: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Usage 4 | # ===== 5 | # 6 | # flux build 7 | # flux start - determine cloud vendor / OS and start 8 | # flux stop - stops 9 | # flux purge - delete everything 10 | # flux pull - pulls latest version 11 | # flux notebook - open browser on notebook start page 12 | # flux ps - list all processes 13 | # flux stats - print resource utilization 14 | # 15 | # Later: 16 | # flux scale / add-host / add-node 17 | # flux update 18 | 19 | function require { 20 | which $1 &>/dev/null || { echo "$1 is not installed, but required!"; exit 1; } 21 | } 22 | 23 | function displayUsage { 24 | grep '^#' $0 | grep -v '#!' | sed 's/^# *//' 25 | } 26 | 27 | function checkForGPU { 28 | true # TODO [ -n `docker volume ls -q -f driver=nvidia-docker` ] 29 | } 30 | 31 | hasGPU=$(checkForGPU) 32 | 33 | set -eo pipefail 34 | 35 | require 'docker' 36 | require 'kubectl' 37 | require 'helm' 38 | 39 | 40 | command=$1 41 | 42 | case "$command" in 43 | "build") 44 | GPU=${hasGPU} || echo '-_gpu' 45 | 46 | echo "suffix: ${GPU}" 47 | 48 | ./deploy/docker/docker_build_ros${GPU}.sh 49 | ./deploy/docker/docker_build${GPU}.sh 50 | docker build -t fluxproject/examples${GPU} . 51 | ;; 52 | "start") 53 | kubectl create -f deploy/kubernetes/flux-ros-hadoop-deployment.yml 54 | kubectl create -f deploy/kubernetes/flux-ros-hadoop-service.yml 55 | ;; 56 | # TODO: poll and block until it is really started? 57 | "purge") 58 | kubectl delete -f deploy/kubernetes/flux-ros-hadoop-service.yml 59 | kubectl delete -f deploy/kubernetes/flux-ros-hadoop-deployment.yml 60 | ;; 61 | "stop") 62 | # TODO: wait for helm install helm delete --purge hdfs 63 | ;; 64 | "ps") 65 | kubectl get all --all-namespaces 66 | ;; 67 | "stats") 68 | echo "TODO: implement me" 69 | exit 1 70 | ;; 71 | "notebook") 72 | #PORT= 73 | open "http://localhost:$(kubectl get service flux-ros-hadoop --template='{{(index .spec.ports 0).nodePort}}')" 74 | ;; 75 | "pull") 76 | docker pull fluxproject/flux:latest 77 | docker pull fluxproject/examples:latest 78 | ;; 79 | 80 | # TODO: need flux init? 81 | # TODO: need flux check to check if everything is running 82 | *) 83 | displayUsage 84 | exit 1 85 | ;; 86 | esac 87 | -------------------------------------------------------------------------------- /bin/test_example: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | POD=$(kubectl get pods --output='go-template={{(index .items 0).metadata.name}}') 3 | 4 | kubectl exec -ti ${POD} -- jupyter nbconvert --execute $1 5 | -------------------------------------------------------------------------------- /deploy/docker/docker_build.sh: -------------------------------------------------------------------------------- 1 | docker build -t fluxproject/flux -f deploy/docker/flux/Dockerfile deploy/docker/flux 2 | -------------------------------------------------------------------------------- /deploy/docker/docker_build_gpu.sh: -------------------------------------------------------------------------------- 1 | docker build -t fluxproject/flux_gpu -f deploy/docker/flux/Dockerfile-gpu deploy/docker/flux 2 | -------------------------------------------------------------------------------- /deploy/docker/docker_build_hdfs.sh: -------------------------------------------------------------------------------- 1 | docker build -t fluxproject/hdfs-nn-4k8s:0.1 -f deploy/docker/hdfs4k8s/Dockerfile-namenode . 2 | docker build -t fluxproject/hdfs-dn-4k8s:0.1 -f deploy/docker/hdfs4k8s/Dockerfile-datanode . 3 | -------------------------------------------------------------------------------- /deploy/docker/docker_build_ros.sh: -------------------------------------------------------------------------------- 1 | docker build -t fluxproject/ros_base -f deploy/docker/ros_base/Dockerfile deploy/docker/ros_base 2 | -------------------------------------------------------------------------------- /deploy/docker/docker_build_ros_gpu.sh: -------------------------------------------------------------------------------- 1 | docker build -t fluxproject/ros_base_gpu -f deploy/docker/ros_base/Dockerfile-gpu deploy/docker/ros_base 2 | -------------------------------------------------------------------------------- /deploy/docker/flux/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM fluxproject/ros_base 2 | 3 | RUN apt-get update && apt-get install -y --no-install-recommends \ 4 | locales bzip2 tree unzip xz-utils curl wget iproute2 sudo \ 5 | python-pip python3-pip python-setuptools python3-setuptools \ 6 | openjdk-8-jdk-headless nodejs npm nodejs-legacy \ 7 | iputils-ping net-tools iproute knot-dnsutils vim \ 8 | ffmpeg \ 9 | && rm -rf /var/lib/apt/lists/* 10 | 11 | # Jupyterhub setting 12 | RUN mkdir -p /etc/jupyterhub 13 | COPY jupyterhub_config.py /etc/jupyterhub/ 14 | 15 | # Introduce flux user # TODO: link 16 | RUN npm install -g configurable-http-proxy 17 | RUN useradd -u 11111 -m -s /bin/bash flux 18 | RUN usermod -aG sudo flux 19 | RUN bash -c " echo flux:flux | chpasswd " 20 | 21 | # Flux user setting # TODO: link 22 | COPY spark-ex-kubernetes.sh /home/flux/ 23 | 24 | RUN python2 -m pip install --upgrade --user pip && \ 25 | python3 -m pip install --upgrade --user pip && \ 26 | python3 -m pip install --no-cache-dir --upgrade jupyter jupyterhub jupyterlab && \ 27 | python2 -m pip install --no-cache-dir --upgrade pyspark matplotlib pandas tensorflow keras Pillow && \ 28 | python2 -m pip install --no-cache-dir --upgrade --force-reinstall requests imageio moviepy seaborn gmaps && \ 29 | python2 -m pip install ipykernel && \ 30 | python2 -m ipykernel install && \ 31 | python3 -m pip install ipykernel && \ 32 | python3 -m ipykernel install 33 | 34 | RUN apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 66F84AE1EB71A8AC108087DCAF677210FF6D3CDA && \ 35 | bash -c 'echo "deb [ arch=amd64 ] http://packages.dataspeedinc.com/ros/ubuntu $(lsb_release -sc) main" > /etc/apt/sources.list.d/ros-dataspeed-public.list' && \ 36 | apt-get update 37 | 38 | RUN bash -c 'echo "yaml http://packages.dataspeedinc.com/ros/ros-public-'$ROS_DISTRO'.yaml '$ROS_DISTRO'" > /etc/ros/rosdep/sources.list.d/30-dataspeed-public-'$ROS_DISTRO'.list' && \ 39 | rosdep update 2>/dev/null && apt-get install -y --no-install-recommends \ 40 | ros-$ROS_DISTRO-dbw-mkz ros-$ROS_DISTRO-mobility-base ros-$ROS_DISTRO-baxter-sdk ros-$ROS_DISTRO-velodyne && \ 41 | rm -rf /var/lib/apt/lists/* 42 | 43 | 44 | # Default to UTF-8 45 | RUN locale-gen en_US.UTF-8 46 | ENV LANG en_US.UTF-8 47 | ENV JAVA_HOME /usr/lib/jvm/java-8-openjdk-amd64 48 | ENV PATH $PATH:/opt/apache/hadoop/bin 49 | ENV ROSIF_JAR /opt/ros_hadoop/master/lib/rosbaginputformat.jar 50 | 51 | RUN mkdir -p /opt/ros_hadoop/master/dist/ 52 | RUN mkdir -p /opt/apache/ 53 | RUN mkdir -p /opt/ros_spark/dist/ 54 | COPY . /opt/ros_hadoop/master/ 55 | 56 | # TODO: ENV ROS_HADOOP='0.9.11' 57 | # RUN \ 58 | # curl -s "https://codeload.github.com/valtech/ros_hadoop/tar.gz/v${ROS_HADOOP}" | \ 59 | # tar -C /opt/ros_hadoop -xvzf - && \ 60 | # mv /opt/ros_hadoop/ros_hadoop-${ROS_HADOOP} /opt/ros_hadoop/latest 61 | RUN \ 62 | curl -s "https://codeload.github.com/valtech/ros_hadoop/tar.gz/master" | \ 63 | tar -C /opt/ros_hadoop -xvzf - && \ 64 | mv /opt/ros_hadoop/ros_hadoop-master /opt/ros_hadoop/latest 65 | 66 | RUN bash -c "if [ ! -f /opt/ros_hadoop/master/dist/hadoop-3.0.0.tar.gz ] ; then wget --no-check-certificate -O /opt/ros_hadoop/master/dist/hadoop-3.1.1.tar.gz -q https://www.eu.apache.org/dist/hadoop/common/hadoop-3.1.1/hadoop-3.1.1.tar.gz ; fi" 67 | RUN tar -xzf /opt/ros_hadoop/master/dist/hadoop-3.1.1.tar.gz -C /opt/apache && rm /opt/ros_hadoop/master/dist/hadoop-3.1.1.tar.gz 68 | RUN ln -s /opt/apache/hadoop-3.1.1 /opt/apache/hadoop 69 | RUN bash -c "if [ ! -f /opt/ros_hadoop/latest/lib/rosbaginputformat.jar ] ; then ln -s /opt/ros_hadoop/master/lib/rosbaginputformat.jar /opt/ros_hadoop/latest/lib/rosbaginputformat.jar ; fi" 70 | 71 | ## for spark example tests 72 | RUN bash -c "if [ ! -f /opt/ros_spark/dist/spark-2.3.1-bin-hadoop2.7.tgz ] ; then wget --quiet -O /opt/ros_spark/dist/spark-2.3.1-bin-hadoop2.7.tgz http://apache.lauf-forum.at/spark/spark-2.3.1/spark-2.3.1-bin-hadoop2.7.tgz ; fi" 73 | RUN tar -xzf /opt/ros_spark/dist/spark-2.3.1-bin-hadoop2.7.tgz -C /opt/apache && rm /opt/ros_spark/dist/spark-2.3.1-bin-hadoop2.7.tgz 74 | 75 | RUN printf "\n\n\nfs.defaultFS\nhdfs://localhost:9000\n\n" > /opt/apache/hadoop/etc/hadoop/core-site.xml && \ 76 | printf "\n\ndfs.replication\n1\n\n" > /opt/apache/hadoop/etc/hadoop/hdfs-site.xml && \ 77 | bash -c "/opt/apache/hadoop/bin/hdfs namenode -format 2>/dev/null" && \ 78 | printf "#! /bin/bash\n/opt/apache/hadoop/bin/hdfs --daemon stop datanode\n/opt/apache/hadoop/bin/hdfs --daemon stop namenode\n/opt/apache/hadoop/bin/hdfs --daemon start namenode\n/opt/apache/hadoop/bin/hdfs --daemon start datanode\nexec \"\$@\"\n" > /start_hadoop.sh && \ 79 | chmod a+x /start_hadoop.sh 80 | 81 | RUN printf "#! /bin/bash\nset -e\nsource \"/opt/ros/$ROS_DISTRO/setup.bash\"\n/start_hadoop.sh\nexec \"\$@\"\n" > /ros_hadoop.sh && \ 82 | chmod a+x /ros_hadoop.sh 83 | 84 | RUN bash -c "if [ ! -f /opt/ros_hadoop/master/dist/HMB_4.bag ] ; then wget --quiet -O /opt/ros_hadoop/master/dist/HMB_4.bag https://xfiles.valtech.io/f/c494d168522045e3bcc0/?dl=1 ; fi" && \ 85 | java -jar "$ROSIF_JAR" -f /opt/ros_hadoop/master/dist/HMB_4.bag 86 | 87 | RUN bash -c "/start_hadoop.sh" && \ 88 | until /opt/apache/hadoop/bin/hdfs dfsadmin -safemode wait; do sleep 1s; done && \ 89 | until /opt/apache/hadoop/bin/hdfs dfsadmin -report; do sleep 1s; done && \ 90 | until /opt/apache/hadoop/bin/hdfs dfs -mkdir /user; do sleep 1s; done && \ 91 | /opt/apache/hadoop/bin/hdfs dfs -mkdir /user/root && \ 92 | /opt/apache/hadoop/bin/hdfs dfs -mkdir /user/flux && \ 93 | /opt/apache/hadoop/bin/hdfs dfs -put /opt/ros_hadoop/master/dist/HMB_4.bag && \ 94 | /opt/apache/hadoop/bin/hdfs --daemon stop datanode && \ 95 | /opt/apache/hadoop/bin/hdfs --daemon stop namenode 96 | 97 | RUN \ 98 | mkdir -p /ope/ros_hadoop/latest/doc && \ 99 | chmod -R 777 /opt/ros_hadoop 100 | 101 | WORKDIR /opt/ros_hadoop/latest/doc/ 102 | ENTRYPOINT ["/ros_hadoop.sh"] 103 | 104 | CMD ["jupyterhub", "-f", "/etc/jupyterhub/jupyterhub_config.py"] 105 | -------------------------------------------------------------------------------- /deploy/docker/flux/Dockerfile-gpu: -------------------------------------------------------------------------------- 1 | FROM fluxproject/ros_base_gpu 2 | 3 | RUN apt-get update && apt-get install -y --no-install-recommends \ 4 | locales bzip2 tree unzip xz-utils curl wget iproute2 sudo \ 5 | python-pip python3-pip python-setuptools python3-setuptools \ 6 | openjdk-8-jdk-headless nodejs npm nodejs-legacy \ 7 | iputils-ping net-tools iproute knot-dnsutils vim \ 8 | ffmpeg \ 9 | && rm -rf /var/lib/apt/lists/* 10 | 11 | # Jupyterhub setting 12 | RUN mkdir -p /etc/jupyterhub 13 | COPY jupyterhub_config.py /etc/jupyterhub/ 14 | 15 | # Introduce flux user 16 | RUN npm install -g configurable-http-proxy 17 | RUN useradd -u 11111 -m -s /bin/bash flux 18 | RUN usermod -aG sudo flux 19 | RUN bash -c " echo flux:flux | chpasswd " 20 | 21 | # Flux user setting # TODO: link 22 | COPY spark-ex-kubernetes.sh /home/flux/ 23 | 24 | RUN python2 -m pip install --upgrade --user pip && \ 25 | python3 -m pip install --upgrade --user pip && \ 26 | python3 -m pip install --no-cache-dir --upgrade jupyter jupyterhub jupyterlab && \ 27 | python2 -m pip install --no-cache-dir --upgrade pyspark matplotlib pandas tensorflow-gpu keras Pillow && \ 28 | python2 -m pip install --no-cache-dir --upgrade --force-reinstall requests imageio moviepy seaborn gmaps && \ 29 | python2 -m pip install ipykernel && \ 30 | python2 -m ipykernel install && \ 31 | python3 -m pip install ipykernel && \ 32 | python3 -m ipykernel install 33 | 34 | RUN apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 66F84AE1EB71A8AC108087DCAF677210FF6D3CDA && \ 35 | bash -c 'echo "deb [ arch=amd64 ] http://packages.dataspeedinc.com/ros/ubuntu $(lsb_release -sc) main" > /etc/apt/sources.list.d/ros-dataspeed-public.list' && \ 36 | apt-get update 37 | 38 | RUN bash -c 'echo "yaml http://packages.dataspeedinc.com/ros/ros-public-'$ROS_DISTRO'.yaml '$ROS_DISTRO'" > /etc/ros/rosdep/sources.list.d/30-dataspeed-public-'$ROS_DISTRO'.list' && \ 39 | rosdep update 2>/dev/null && apt-get install -y --no-install-recommends \ 40 | ros-$ROS_DISTRO-dbw-mkz ros-$ROS_DISTRO-mobility-base ros-$ROS_DISTRO-baxter-sdk ros-$ROS_DISTRO-velodyne && \ 41 | rm -rf /var/lib/apt/lists/* 42 | 43 | # Default to UTF-8 44 | RUN locale-gen en_US.UTF-8 45 | ENV LANG en_US.UTF-8 46 | ENV JAVA_HOME /usr/lib/jvm/java-8-openjdk-amd64 47 | ENV PATH $PATH:/opt/apache/hadoop/bin 48 | ENV ROSIF_JAR /opt/ros_hadoop/master/lib/rosbaginputformat.jar 49 | 50 | RUN mkdir -p /opt/ros_hadoop/latest 51 | RUN mkdir -p /opt/ros_hadoop/master/dist/ 52 | RUN mkdir -p /opt/apache/ 53 | RUN mkdir -p /opt/ros_spark/dist/ 54 | COPY . /opt/ros_hadoop/master/ 55 | RUN bash -c "curl -s https://api.github.com/repos/valtech/ros_hadoop/releases/latest | egrep -io 'https://api.github.com/repos/valtech/ros_hadoop/tarball/[^\"]*' | xargs wget --quiet -O /opt/ros_hadoop/latest.tgz" 56 | RUN bash -c "if [ ! -f /opt/ros_hadoop/master/dist/hadoop-3.0.0.tar.gz ] ; then wget --no-check-certificate -O /opt/ros_hadoop/master/dist/hadoop-3.1.1.tar.gz -q https://www.eu.apache.org/dist/hadoop/common/hadoop-3.1.1/hadoop-3.1.1.tar.gz ; fi" 57 | RUN tar -xzf /opt/ros_hadoop/latest.tgz -C /opt/ros_hadoop/latest --strip-components=1 && rm /opt/ros_hadoop/latest.tgz 58 | RUN tar -xzf /opt/ros_hadoop/master/dist/hadoop-3.1.1.tar.gz -C /opt/apache && rm /opt/ros_hadoop/master/dist/hadoop-3.1.1.tar.gz 59 | RUN ln -s /opt/apache/hadoop-3.1.1 /opt/apache/hadoop 60 | RUN bash -c "if [ ! -f /opt/ros_hadoop/latest/lib/rosbaginputformat.jar ] ; then ln -s /opt/ros_hadoop/master/lib/rosbaginputformat.jar /opt/ros_hadoop/latest/lib/rosbaginputformat.jar ; fi" 61 | 62 | ## for spark example tests 63 | RUN bash -c "if [ ! -f /opt/ros_spark/dist/spark-2.3.1-bin-hadoop2.7.tgz ] ; then wget --quiet -O /opt/ros_spark/dist/spark-2.3.1-bin-hadoop2.7.tgz http://apache.lauf-forum.at/spark/spark-2.3.1/spark-2.3.1-bin-hadoop2.7.tgz ; fi" 64 | RUN tar -xzf /opt/ros_spark/dist/spark-2.3.1-bin-hadoop2.7.tgz -C /opt/apache && rm /opt/ros_spark/dist/spark-2.3.1-bin-hadoop2.7.tgz 65 | 66 | RUN printf "\n\n\nfs.defaultFS\nhdfs://localhost:9000\n\n" > /opt/apache/hadoop/etc/hadoop/core-site.xml && \ 67 | printf "\n\ndfs.replication\n1\n\n" > /opt/apache/hadoop/etc/hadoop/hdfs-site.xml && \ 68 | bash -c "/opt/apache/hadoop/bin/hdfs namenode -format 2>/dev/null" && \ 69 | printf "#! /bin/bash\n/opt/apache/hadoop/bin/hdfs --daemon stop datanode\n/opt/apache/hadoop/bin/hdfs --daemon stop namenode\n/opt/apache/hadoop/bin/hdfs --daemon start namenode\n/opt/apache/hadoop/bin/hdfs --daemon start datanode\nexec \"\$@\"\n" > /start_hadoop.sh && \ 70 | chmod a+x /start_hadoop.sh 71 | 72 | RUN printf "#! /bin/bash\nset -e\nsource \"/opt/ros/$ROS_DISTRO/setup.bash\"\n/start_hadoop.sh\nexec \"\$@\"\n" > /ros_hadoop.sh && \ 73 | chmod a+x /ros_hadoop.sh 74 | 75 | RUN bash -c "if [ ! -f /opt/ros_hadoop/master/dist/HMB_4.bag ] ; then wget --quiet -O /opt/ros_hadoop/master/dist/HMB_4.bag https://xfiles.valtech.io/f/c494d168522045e3bcc0/?dl=1 ; fi" && \ 76 | java -jar "$ROSIF_JAR" -f /opt/ros_hadoop/master/dist/HMB_4.bag 77 | 78 | RUN bash -c "/start_hadoop.sh" && \ 79 | /opt/apache/hadoop/bin/hdfs dfsadmin -safemode wait && \ 80 | /opt/apache/hadoop/bin/hdfs dfsadmin -report && \ 81 | /opt/apache/hadoop/bin/hdfs dfs -mkdir /user && \ 82 | /opt/apache/hadoop/bin/hdfs dfs -mkdir /user/root && \ 83 | /opt/apache/hadoop/bin/hdfs dfs -mkdir /user/flux && \ 84 | /opt/apache/hadoop/bin/hdfs dfs -put /opt/ros_hadoop/master/dist/HMB_4.bag && \ 85 | /opt/apache/hadoop/bin/hdfs --daemon stop datanode && \ 86 | /opt/apache/hadoop/bin/hdfs --daemon stop namenode 87 | 88 | RUN \ 89 | mkdir -p /ope/ros_hadoop/latest/doc && \ 90 | chmod -R 777 /opt/ros_hadoop 91 | 92 | WORKDIR /opt/ros_hadoop/latest/doc/ 93 | ENTRYPOINT ["/ros_hadoop.sh"] 94 | 95 | CMD ["jupyterhub", "-f", "/etc/jupyterhub/jupyterhub_config.py"] 96 | -------------------------------------------------------------------------------- /deploy/docker/flux/README.md: -------------------------------------------------------------------------------- 1 | # **RosbagInputFormat** 2 | RosbagInputFormat is an open source **splittable** Hadoop InputFormat for the ROS bag file format. 3 | 4 | The complete source code is available in src/ folder and the jar file is generated using SBT (see build.sbt) 5 | 6 | For an example of rosbag file larger than 2 GB see doc/Rosbag larger than 2 GB.ipynb Solved the issue https://github.com/valtech/ros_hadoop/issues/6 The issue was due to ByteBuffer being limitted by JVM Integer size and has nothing to do with Spark or how the RosbagMapInputFormat works within Spark. It was only problematic to extract the conf index with the jar. 7 | 8 | # Usage 9 | 10 | 1. Download latest release jar file and put it in classpath 11 | 2. Extract the index configuration of your ROS bag file. **The extracted index is a very very small configuration** file containing a protobuf array that will be given in the job configuration. **Note that the operation will not process and it will not parse** the whole bag file, but will simply seek to the required offset. e.g. 12 | ```bash 13 | java -jar lib/rosbaginputformat.jar -f /opt/ros_hadoop/master/dist/HMB_4.bag 14 | # will create an idx.bin config file /opt/ros_hadoop/master/dist/HMB_4.bag.idx.bin 15 | ``` 16 | 3. Put the ROS bag file in HDFS e.g. 17 | ```bash 18 | hdfs dfs -put 19 | ``` 20 | 4. Use it in your Spark jobs e.g. 21 | ```python 22 | sc.newAPIHadoopFile( 23 | path = "hdfs://127.0.0.1:9000/user/spark/HMB_4.bag", 24 | inputFormatClass = "de.valtech.foss.RosbagMapInputFormat", 25 | keyClass = "org.apache.hadoop.io.LongWritable", 26 | valueClass = "org.apache.hadoop.io.MapWritable", 27 | conf = {"RosbagInputFormat.chunkIdx":"/opt/ros_hadoop/master/dist/HMB_4.bag.idx.bin"}) 28 | ``` 29 | 30 | Example data can be found for instance at https://github.com/udacity/self-driving-car/tree/master/datasets published under MIT License. 31 | 32 | # Documentation 33 | The [doc/](doc/) folder contains a jupyter notebook with a few basic usage examples. 34 | 35 |

36 |

37 | 38 | # Tutorial 39 | 40 | ## To test locally use the Dockerfile 41 | 42 | To build an image using the Dockerfile run the following in the shell. 43 | Please note that it will download Hadoop and Spark from the URL source. The generated image is therefore relatively large ~5G. 44 | ```bash 45 | docker build -t ros_hadoop:latest -f Dockerfile . 46 | ``` 47 | 48 | To start a container use the following shell command **in the ros_hadoop folder.** 49 | ```bash 50 | # $(pwd) will point to the ros_hadoop git clone folder 51 | docker run -it -v $(pwd):/root/ros_hadoop -p 8888:8888 ros_hadoop 52 | ``` 53 | The container has a configured HDFS as well as Spark and the RosInputFormat jar. 54 | It leaves the user in a bash shell. 55 | 56 | Point your browser to the local [URL](http://localhost:8888/) and enjoy the tutorial. The access token is printed in the docker container console. 57 | 58 | ### Usage from Spark (pyspark) 59 | 60 | Example data can be found for instance at https://github.com/udacity/self-driving-car/tree/master/datasets published under MIT License. 61 | 62 | Check that the Rosbag file version is V2.0 63 | 64 | ```bash 65 | java -jar lib/rosbaginputformat.jar --version -f /opt/ros_hadoop/master/dist/HMB_4.bag 66 | ``` 67 | 68 | ### Extract the index as configuration 69 | 70 | The index is a very very small configuration file containing a protobuf array that will be given in the job configuration. 71 | Note that the operation will not process and it will not parse the whole bag file, but will simply seek to the required offset. 72 | 73 | ```bash 74 | # assuming you start the notebook in the doc/ folder 75 | java -jar ../lib/rosbaginputformat.jar \ 76 | -f /opt/ros_hadoop/master/dist/HMB_4.bag 77 | 78 | hdfs dfs -ls 79 | ``` 80 | 81 | This will generate a very small file named HMB_4.bag.idx.bin in the same folder. 82 | 83 | ### Copy the bag file in HDFS 84 | 85 | Using your favorite tool put the bag file in your working HDFS folder. 86 | 87 | ***Note***: keep the index file as configuration to your jobs, ***do not*** put small files in HDFS. 88 | For convenience we already provide an example file (/opt/ros_hadoop/master/dist/HMB_4.bag) in the HDFS under /user/root/ 89 | 90 | ```bash 91 | hdfs dfs -put /opt/ros_hadoop/master/dist/HMB_4.bag 92 | hdfs dfs -ls 93 | ``` 94 |

95 |

96 | 97 | + Hadoop InputFormat and Record Reader for Rosbag 98 | + Process Rosbag with Spark, Yarn, MapReduce, Hadoop Streaming API, … 99 | + Spark RDD are cached and optimised for analysis 100 | 101 | ### Process the ROS bag file in Spark using the RosbagInputFormat 102 | 103 | ***Note***: your HDFS address might differ. 104 | ```python 105 | fin = sc.newAPIHadoopFile( 106 | path = "hdfs://127.0.0.1:9000/user/root/HMB_4.bag", 107 | inputFormatClass = "de.valtech.foss.RosbagMapInputFormat", 108 | keyClass = "org.apache.hadoop.io.LongWritable", 109 | valueClass = "org.apache.hadoop.io.MapWritable", 110 | conf = {“RosbagInputFormat.chunkIdx”:”/opt/ros_hadoop/master/dist/HMB_4.bag.idx.bin"}) 111 | ``` 112 | 113 | ### Interpret the Messages 114 | 115 | To interpret the messages we need the connections. 116 | We could get the connections as configuration as well. At the moment we decided to collect the connections into Spark driver in a dictionary and use it in the subsequent RDD actions. 117 | 118 | Collect the connections from all Spark partitions of the bag file into the Spark driver 119 | ```python 120 | conn_a = fin.filter( 121 | lambda r: r[1]['header']['op'] == 7 122 | ).map( 123 | lambda r: r[1] 124 | ).collect() 125 | conn_d = {str(k['header']['topic']):k for k in conn_a} 126 | 127 | # see topic names 128 | conn_d.keys() 129 | ``` 130 | 131 | From all ROS bag splits we collect into Spark driver the connection messages (op=7 in header) where the ROS definitions are stored. This operation happens in parallel of course. 132 | 133 | ### Load the python map functions from src/main/python/functions.py 134 | ```bash 135 | %run -i ../src/main/python/functions.py 136 | ``` 137 | At the moment the file contains a single mapper function named msg_map. 138 | 139 | ### Use of msg_map to apply a function on all messages 140 | 141 | Python rosbag.bag needs to be installed on all Spark workers. The msg_map function (from src/main/python/functions.py) takes three arguments: 142 | 1. r = the message or RDD record Tuple 143 | 2. func = a function (default str) to apply to the ROS message 144 | 3. conn = a connection to specify what topic to process 145 | 146 | ```python 147 | %matplotlib nbagg 148 | # use %matplotlib notebook in python3 149 | from functools import partial 150 | import pandas as pd 151 | import numpy as np 152 | 153 | 154 | # Take messages from '/imu/data' topic using default str func 155 | rdd = fin.flatMap( 156 | partial(msg_map, conn=conn_d['/imu/data']) 157 | ) 158 | ``` 159 | 160 | The connection dictionary is sent over the closure to the workers that uses it in the msg_map. 161 | 162 |

163 | 164 | ```python 165 | print(rdd.take(1)[0]) 166 | ``` 167 | 168 | ``` 169 | header: 170 | seq: 1701626 171 | stamp: 172 | secs: 1479425728 173 | nsecs: 747487068 174 | frame_id: /imu 175 | orientation: 176 | x: -0.0251433756238 177 | y: 0.0284643176884 178 | z: -0.0936542998233 179 | w: 0.994880191333 180 | orientation_covariance: [0.017453292519943295, 0.0, 0.0, 0.0, 0.017453292519943295, 0.0, 0.0, 0.0, 0.15707963267948966] 181 | angular_velocity: 182 | x: 0.0 183 | y: 0.0 184 | z: 0.0 185 | angular_velocity_covariance: [-1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0] 186 | linear_acceleration: 187 | x: 1.16041922569 188 | y: 0.595418334007 189 | z: 10.7565326691 190 | linear_acceleration_covariance: [0.0004, 0.0, 0.0, 0.0, 0.0004, 0.0, 0.0, 0.0, 0.0004] 191 | ``` 192 | 193 | 194 | ### Image data from camera messages 195 | 196 | An example of taking messages using a func other than default str. 197 | In our case we apply a lambda to messages from from '/center_camera/image_color/compressed' topic. As usual with Spark the operation will happen in parallel on all workers. 198 | 199 | ```python 200 | from PIL import Image 201 | from io import BytesIO 202 | 203 | res = fin.flatMap( 204 | partial(msg_map, func=lambda r: r.data, conn=conn_d['/center_camera/image_color/compressed']) 205 | ).take(50) 206 | 207 | Image.open(BytesIO(res[48])) 208 | ``` 209 | 210 |

211 | 212 | 213 | ### Plot fuel level 214 | 215 | The topic /vehicle/fuel_level_report contains 2215 ROS messages. Let us plot the header.stamp in seconds vs. fuel_level using a pandas dataframe. 216 | 217 | ```python 218 | def f(msg): 219 | return (msg.header.stamp.secs, msg.fuel_level) 220 | 221 | d = fin.flatMap( 222 | partial(msg_map, func=f, conn=conn_d['/vehicle/fuel_level_report']) 223 | ).toDF().toPandas() 224 | 225 | d.set_index(‘_1').plot() 226 | ``` 227 |

228 | 229 | ### Machine Learning models on Spark workers 230 | 231 | A dot product Keras "model" for each message from a topic. We will compare it with the one computed with numpy. 232 | 233 | ***Note*** that the imports happen in the workers and not in driver. On the other hand the connection dictionary is sent over the closure. 234 | 235 | ```python 236 | def f(msg): 237 | from keras.layers import dot, Dot, Input 238 | from keras.models import Model 239 | 240 | linear_acceleration = { 241 | 'x': msg.linear_acceleration.x, 242 | 'y': msg.linear_acceleration.y, 243 | 'z': msg.linear_acceleration.z, 244 | } 245 | 246 | linear_acceleration_covariance = np.array(msg.linear_acceleration_covariance) 247 | 248 | i1 = Input(shape=(3,)) 249 | i2 = Input(shape=(3,)) 250 | o = dot([i1,i2], axes=1) 251 | 252 | model = Model([i1,i2], o) 253 | 254 | # return a tuple with (numpy dot product, keras dot "predict") 255 | return ( 256 | np.dot(linear_acceleration_covariance.reshape(3,3), 257 | [linear_acceleration['x'], linear_acceleration['y'], linear_acceleration['z']]), 258 | model.predict([ 259 | np.array([[ linear_acceleration['x'], linear_acceleration['y'], linear_acceleration['z'] ]]), 260 | linear_acceleration_covariance.reshape((3,3))]) 261 | ) 262 | 263 | fin.flatMap(partial(msg_map, func=f, conn=conn_d['/vehicle/imu/data_raw'])).take(5) 264 | 265 | # tuple with (numpy dot product, keras dot “predict”) 266 | ``` 267 | One can sample of course and collect the data in the driver to train a model on one single machine. 268 | Note that the msg is the most granular unit but you could replace the flatMap with a mapPartitions to apply such a Keras function to a whole split. 269 | 270 | Another option would be to have a map.reduceByKey before the flatMap so that the function argument would be a whole interval instead of a msg. The idea is to key on time. 271 | 272 | We hope that the RosbagInputFormat would be useful to you. 273 | 274 | ## Please do not forget to send us your [feedback](AUTHORS). 275 | ![doc/images/browse-tutorial.png](doc/images/browse-tutorial.png) 276 | -------------------------------------------------------------------------------- /deploy/docker/flux/jupyterhub_config.py: -------------------------------------------------------------------------------- 1 | # Configuration file for jupyterhub. 2 | 3 | #------------------------------------------------------------------------------ 4 | # Application(SingletonConfigurable) configuration 5 | #------------------------------------------------------------------------------ 6 | 7 | ## This is an application. 8 | 9 | ## The date format used by logging formatters for %(asctime)s 10 | #c.Application.log_datefmt = '%Y-%m-%d %H:%M:%S' 11 | 12 | ## The Logging format template 13 | #c.Application.log_format = '[%(name)s]%(highlevel)s %(message)s' 14 | 15 | ## Set the log level by value or name. 16 | #c.Application.log_level = 30 17 | 18 | #------------------------------------------------------------------------------ 19 | # JupyterHub(Application) configuration 20 | #------------------------------------------------------------------------------ 21 | 22 | ## An Application for starting a Multi-User Jupyter Notebook server. 23 | 24 | ## Maximum number of concurrent servers that can be active at a time. 25 | # 26 | # Setting this can limit the total resources your users can consume. 27 | # 28 | # An active server is any server that's not fully stopped. It is considered 29 | # active from the time it has been requested until the time that it has 30 | # completely stopped. 31 | # 32 | # If this many user servers are active, users will not be able to launch new 33 | # servers until a server is shutdown. Spawn requests will be rejected with a 429 34 | # error asking them to try again. 35 | # 36 | # If set to 0, no limit is enforced. 37 | #c.JupyterHub.active_server_limit = 0 38 | 39 | ## Grant admin users permission to access single-user servers. 40 | # 41 | # Users should be properly informed if this is enabled. 42 | #c.JupyterHub.admin_access = False 43 | 44 | ## DEPRECATED since version 0.7.2, use Authenticator.admin_users instead. 45 | #c.JupyterHub.admin_users = set() 46 | 47 | ## Allow named single-user servers per user 48 | #c.JupyterHub.allow_named_servers = False 49 | 50 | ## Answer yes to any questions (e.g. confirm overwrite) 51 | #c.JupyterHub.answer_yes = False 52 | 53 | ## PENDING DEPRECATION: consider using service_tokens 54 | # 55 | # Dict of token:username to be loaded into the database. 56 | # 57 | # Allows ahead-of-time generation of API tokens for use by externally managed 58 | # services, which authenticate as JupyterHub users. 59 | # 60 | # Consider using service_tokens for general services that talk to the JupyterHub 61 | # API. 62 | #c.JupyterHub.api_tokens = {} 63 | 64 | ## Class for authenticating users. 65 | # 66 | # This should be a class with the following form: 67 | # 68 | # - constructor takes one kwarg: `config`, the IPython config object. 69 | # 70 | # - is a tornado.gen.coroutine 71 | # - returns username on success, None on failure 72 | # - takes two arguments: (handler, data), 73 | # where `handler` is the calling web.RequestHandler, 74 | # and `data` is the POST form data from the login page. 75 | #c.JupyterHub.authenticator_class = 'jupyterhub.auth.PAMAuthenticator' 76 | 77 | ## The base URL of the entire application 78 | #c.JupyterHub.base_url = '/' 79 | 80 | ## Whether to shutdown the proxy when the Hub shuts down. 81 | # 82 | # Disable if you want to be able to teardown the Hub while leaving the proxy 83 | # running. 84 | # 85 | # Only valid if the proxy was starting by the Hub process. 86 | # 87 | # If both this and cleanup_servers are False, sending SIGINT to the Hub will 88 | # only shutdown the Hub, leaving everything else running. 89 | # 90 | # The Hub should be able to resume from database state. 91 | #c.JupyterHub.cleanup_proxy = True 92 | 93 | ## Whether to shutdown single-user servers when the Hub shuts down. 94 | # 95 | # Disable if you want to be able to teardown the Hub while leaving the single- 96 | # user servers running. 97 | # 98 | # If both this and cleanup_proxy are False, sending SIGINT to the Hub will only 99 | # shutdown the Hub, leaving everything else running. 100 | # 101 | # The Hub should be able to resume from database state. 102 | #c.JupyterHub.cleanup_servers = True 103 | 104 | ## Maximum number of concurrent users that can be spawning at a time. 105 | # 106 | # Spawning lots of servers at the same time can cause performance problems for 107 | # the Hub or the underlying spawning system. Set this limit to prevent bursts of 108 | # logins from attempting to spawn too many servers at the same time. 109 | # 110 | # This does not limit the number of total running servers. See 111 | # active_server_limit for that. 112 | # 113 | # If more than this many users attempt to spawn at a time, their requests will 114 | # be rejected with a 429 error asking them to try again. Users will have to wait 115 | # for some of the spawning services to finish starting before they can start 116 | # their own. 117 | # 118 | # If set to 0, no limit is enforced. 119 | #c.JupyterHub.concurrent_spawn_limit = 100 120 | 121 | ## The config file to load 122 | #c.JupyterHub.config_file = 'jupyterhub_config.py' 123 | 124 | ## DEPRECATED: does nothing 125 | #c.JupyterHub.confirm_no_ssl = False 126 | 127 | ## Number of days for a login cookie to be valid. Default is two weeks. 128 | #c.JupyterHub.cookie_max_age_days = 14 129 | 130 | ## The cookie secret to use to encrypt cookies. 131 | # 132 | # Loaded from the JPY_COOKIE_SECRET env variable by default. 133 | # 134 | # Should be exactly 256 bits (32 bytes). 135 | #c.JupyterHub.cookie_secret = b'' 136 | 137 | ## File in which to store the cookie secret. 138 | #c.JupyterHub.cookie_secret_file = 'jupyterhub_cookie_secret' 139 | 140 | ## The location of jupyterhub data files (e.g. /usr/local/share/jupyter/hub) 141 | #c.JupyterHub.data_files_path = '/usr/local/share/jupyter/hub' 142 | 143 | ## Include any kwargs to pass to the database connection. See 144 | # sqlalchemy.create_engine for details. 145 | #c.JupyterHub.db_kwargs = {} 146 | 147 | ## url for the database. e.g. `sqlite:///jupyterhub.sqlite` 148 | #c.JupyterHub.db_url = 'sqlite:///jupyterhub.sqlite' 149 | 150 | ## log all database transactions. This has A LOT of output 151 | #c.JupyterHub.debug_db = False 152 | 153 | ## DEPRECATED since version 0.8: Use ConfigurableHTTPProxy.debug 154 | #c.JupyterHub.debug_proxy = False 155 | 156 | ## Send JupyterHub's logs to this file. 157 | # 158 | # This will *only* include the logs of the Hub itself, not the logs of the proxy 159 | # or any single-user servers. 160 | #c.JupyterHub.extra_log_file = '' 161 | 162 | ## Extra log handlers to set on JupyterHub logger 163 | #c.JupyterHub.extra_log_handlers = [] 164 | 165 | ## Generate default config file 166 | #c.JupyterHub.generate_config = False 167 | 168 | ## The ip or hostname for proxies and spawners to use for connecting to the Hub. 169 | # 170 | # Use when the bind address (`hub_ip`) is 0.0.0.0 or otherwise different from 171 | # the connect address. 172 | # 173 | # Default: when `hub_ip` is 0.0.0.0, use `socket.gethostname()`, otherwise use 174 | # `hub_ip`. 175 | # 176 | # .. versionadded:: 0.8 177 | #c.JupyterHub.hub_connect_ip = '' 178 | 179 | ## The port for proxies & spawners to connect to the hub on. 180 | # 181 | # Used alongside `hub_connect_ip` 182 | # 183 | # .. versionadded:: 0.8 184 | #c.JupyterHub.hub_connect_port = 0 185 | 186 | ## The ip address for the Hub process to *bind* to. 187 | # 188 | # See `hub_connect_ip` for cases where the bind and connect address should 189 | # differ. 190 | #c.JupyterHub.hub_ip = '127.0.0.1' 191 | 192 | ## The port for the Hub process 193 | #c.JupyterHub.hub_port = 8081 194 | 195 | ## The public facing ip of the whole application (the proxy) 196 | #c.JupyterHub.ip = '' 197 | 198 | ## Supply extra arguments that will be passed to Jinja environment. 199 | #c.JupyterHub.jinja_environment_options = {} 200 | 201 | ## Interval (in seconds) at which to update last-activity timestamps. 202 | #c.JupyterHub.last_activity_interval = 300 203 | 204 | ## Dict of 'group': ['usernames'] to load at startup. 205 | # 206 | # This strictly *adds* groups and users to groups. 207 | # 208 | # Loading one set of groups, then starting JupyterHub again with a different set 209 | # will not remove users or groups from previous launches. That must be done 210 | # through the API. 211 | #c.JupyterHub.load_groups = {} 212 | 213 | ## Specify path to a logo image to override the Jupyter logo in the banner. 214 | #c.JupyterHub.logo_file = '' 215 | 216 | ## File to write PID Useful for daemonizing jupyterhub. 217 | #c.JupyterHub.pid_file = '' 218 | 219 | ## The public facing port of the proxy 220 | #c.JupyterHub.port = 8000 221 | 222 | ## DEPRECATED since version 0.8 : Use ConfigurableHTTPProxy.api_url 223 | #c.JupyterHub.proxy_api_ip = '' 224 | 225 | ## DEPRECATED since version 0.8 : Use ConfigurableHTTPProxy.api_url 226 | #c.JupyterHub.proxy_api_port = 0 227 | 228 | ## DEPRECATED since version 0.8: Use ConfigurableHTTPProxy.auth_token 229 | #c.JupyterHub.proxy_auth_token = '' 230 | 231 | ## Interval (in seconds) at which to check if the proxy is running. 232 | #c.JupyterHub.proxy_check_interval = 30 233 | 234 | ## Select the Proxy API implementation. 235 | #c.JupyterHub.proxy_class = 'jupyterhub.proxy.ConfigurableHTTPProxy' 236 | 237 | ## DEPRECATED since version 0.8. Use ConfigurableHTTPProxy.command 238 | #c.JupyterHub.proxy_cmd = [] 239 | 240 | ## Purge and reset the database. 241 | #c.JupyterHub.reset_db = False 242 | 243 | ## Interval (in seconds) at which to check connectivity of services with web 244 | # endpoints. 245 | #c.JupyterHub.service_check_interval = 60 246 | 247 | ## Dict of token:servicename to be loaded into the database. 248 | # 249 | # Allows ahead-of-time generation of API tokens for use by externally managed 250 | # services. 251 | #c.JupyterHub.service_tokens = {} 252 | 253 | ## List of service specification dictionaries. 254 | # 255 | # A service 256 | # 257 | # For instance:: 258 | # 259 | # services = [ 260 | # { 261 | # 'name': 'cull_idle', 262 | # 'command': ['/path/to/cull_idle_servers.py'], 263 | # }, 264 | # { 265 | # 'name': 'formgrader', 266 | # 'url': 'http://127.0.0.1:1234', 267 | # 'api_token': 'super-secret', 268 | # 'environment': 269 | # } 270 | # ] 271 | #c.JupyterHub.services = [] 272 | 273 | ## The class to use for spawning single-user servers. 274 | # 275 | # Should be a subclass of Spawner. 276 | #c.JupyterHub.spawner_class = 'jupyterhub.spawner.LocalProcessSpawner' 277 | 278 | ## Path to SSL certificate file for the public facing interface of the proxy 279 | # 280 | # When setting this, you should also set ssl_key 281 | #c.JupyterHub.ssl_cert = '' 282 | 283 | ## Path to SSL key file for the public facing interface of the proxy 284 | # 285 | # When setting this, you should also set ssl_cert 286 | #c.JupyterHub.ssl_key = '' 287 | 288 | ## Host to send statsd metrics to 289 | #c.JupyterHub.statsd_host = '' 290 | 291 | ## Port on which to send statsd metrics about the hub 292 | #c.JupyterHub.statsd_port = 8125 293 | 294 | ## Prefix to use for all metrics sent by jupyterhub to statsd 295 | #c.JupyterHub.statsd_prefix = 'jupyterhub' 296 | 297 | ## Run single-user servers on subdomains of this host. 298 | # 299 | # This should be the full `https://hub.domain.tld[:port]`. 300 | # 301 | # Provides additional cross-site protections for javascript served by single- 302 | # user servers. 303 | # 304 | # Requires `.hub.domain.tld` to resolve to the same host as 305 | # `hub.domain.tld`. 306 | # 307 | # In general, this is most easily achieved with wildcard DNS. 308 | # 309 | # When using SSL (i.e. always) this also requires a wildcard SSL certificate. 310 | #c.JupyterHub.subdomain_host = '' 311 | 312 | ## Paths to search for jinja templates. 313 | #c.JupyterHub.template_paths = [] 314 | 315 | ## Extra settings overrides to pass to the tornado application. 316 | #c.JupyterHub.tornado_settings = {} 317 | 318 | ## Trust user-provided tokens (via JupyterHub.service_tokens) to have good 319 | # entropy. 320 | # 321 | # If you are not inserting additional tokens via configuration file, this flag 322 | # has no effect. 323 | # 324 | # In JupyterHub 0.8, internally generated tokens do not pass through additional 325 | # hashing because the hashing is costly and does not increase the entropy of 326 | # already-good UUIDs. 327 | # 328 | # User-provided tokens, on the other hand, are not trusted to have good entropy 329 | # by default, and are passed through many rounds of hashing to stretch the 330 | # entropy of the key (i.e. user-provided tokens are treated as passwords instead 331 | # of random keys). These keys are more costly to check. 332 | # 333 | # If your inserted tokens are generated by a good-quality mechanism, e.g. 334 | # `openssl rand -hex 32`, then you can set this flag to True to reduce the cost 335 | # of checking authentication tokens. 336 | #c.JupyterHub.trust_user_provided_tokens = False 337 | 338 | ## Upgrade the database automatically on start. 339 | # 340 | # Only safe if database is regularly backed up. Only SQLite databases will be 341 | # backed up to a local file automatically. 342 | #c.JupyterHub.upgrade_db = False 343 | 344 | #------------------------------------------------------------------------------ 345 | # Spawner(LoggingConfigurable) configuration 346 | #------------------------------------------------------------------------------ 347 | 348 | ## Base class for spawning single-user notebook servers. 349 | # 350 | # Subclass this, and override the following methods: 351 | # 352 | # - load_state - get_state - start - stop - poll 353 | # 354 | # As JupyterHub supports multiple users, an instance of the Spawner subclass is 355 | # created for each user. If there are 20 JupyterHub users, there will be 20 356 | # instances of the subclass. 357 | 358 | ## Extra arguments to be passed to the single-user server. 359 | # 360 | # Some spawners allow shell-style expansion here, allowing you to use 361 | # environment variables here. Most, including the default, do not. Consult the 362 | # documentation for your spawner to verify! 363 | #c.Spawner.args = [] 364 | 365 | ## The command used for starting the single-user server. 366 | # 367 | # Provide either a string or a list containing the path to the startup script 368 | # command. Extra arguments, other than this path, should be provided via `args`. 369 | # 370 | # This is usually set if you want to start the single-user server in a different 371 | # python environment (with virtualenv/conda) than JupyterHub itself. 372 | # 373 | # Some spawners allow shell-style expansion here, allowing you to use 374 | # environment variables. Most, including the default, do not. Consult the 375 | # documentation for your spawner to verify! 376 | #c.Spawner.cmd = ['jupyterhub-singleuser'] 377 | 378 | ## Minimum number of cpu-cores a single-user notebook server is guaranteed to 379 | # have available. 380 | # 381 | # If this value is set to 0.5, allows use of 50% of one CPU. If this value is 382 | # set to 2, allows use of up to 2 CPUs. 383 | # 384 | # Note that this needs to be supported by your spawner for it to work. 385 | #c.Spawner.cpu_guarantee = None 386 | 387 | ## Maximum number of cpu-cores a single-user notebook server is allowed to use. 388 | # 389 | # If this value is set to 0.5, allows use of 50% of one CPU. If this value is 390 | # set to 2, allows use of up to 2 CPUs. 391 | # 392 | # The single-user notebook server will never be scheduled by the kernel to use 393 | # more cpu-cores than this. There is no guarantee that it can access this many 394 | # cpu-cores. 395 | # 396 | # This needs to be supported by your spawner for it to work. 397 | #c.Spawner.cpu_limit = None 398 | 399 | ## Enable debug-logging of the single-user server 400 | #c.Spawner.debug = False 401 | 402 | ## The URL the single-user server should start in. 403 | # 404 | # `{username}` will be expanded to the user's username 405 | # 406 | # Example uses: 407 | # 408 | # - You can set `notebook_dir` to `/` and `default_url` to `/tree/home/{username}` to allow people to 409 | # navigate the whole filesystem from their notebook server, but still start in their home directory. 410 | # - Start with `/notebooks` instead of `/tree` if `default_url` points to a notebook instead of a directory. 411 | # - You can set this to `/lab` to have JupyterLab start by default, rather than Jupyter Notebook. 412 | c.Spawner.default_url = '/lab' 413 | 414 | ## Disable per-user configuration of single-user servers. 415 | # 416 | # When starting the user's single-user server, any config file found in the 417 | # user's $HOME directory will be ignored. 418 | # 419 | # Note: a user could circumvent this if the user modifies their Python 420 | # environment, such as when they have their own conda environments / virtualenvs 421 | # / containers. 422 | #c.Spawner.disable_user_config = False 423 | 424 | ## Whitelist of environment variables for the single-user server to inherit from 425 | # the JupyterHub process. 426 | # 427 | # This whitelist is used to ensure that sensitive information in the JupyterHub 428 | # process's environment (such as `CONFIGPROXY_AUTH_TOKEN`) is not passed to the 429 | # single-user server's process. 430 | #c.Spawner.env_keep = ['PATH', 'PYTHONPATH', 'CONDA_ROOT', 'CONDA_DEFAULT_ENV', 'VIRTUAL_ENV', 'LANG', 'LC_ALL'] 431 | c.Spawner.env_keep = ['PATH', 'PYTHONPATH', 'LANG', 'LC_ALL', 'JAVA_HOME', 'ROSIF_JAR', 'HOSTNAME', 'TERM', 'ROS_DISTRO'] 432 | 433 | ## Extra environment variables to set for the single-user server's process. 434 | # 435 | # Environment variables that end up in the single-user server's process come from 3 sources: 436 | # - This `environment` configurable 437 | # - The JupyterHub process' environment variables that are whitelisted in `env_keep` 438 | # - Variables to establish contact between the single-user notebook and the hub (such as JUPYTERHUB_API_TOKEN) 439 | # 440 | # The `enviornment` configurable should be set by JupyterHub administrators to 441 | # add installation specific environment variables. It is a dict where the key is 442 | # the name of the environment variable, and the value can be a string or a 443 | # callable. If it is a callable, it will be called with one parameter (the 444 | # spawner instance), and should return a string fairly quickly (no blocking 445 | # operations please!). 446 | # 447 | # Note that the spawner class' interface is not guaranteed to be exactly same 448 | # across upgrades, so if you are using the callable take care to verify it 449 | # continues to work after upgrades! 450 | #c.Spawner.environment = {} 451 | 452 | ## Timeout (in seconds) before giving up on a spawned HTTP server 453 | # 454 | # Once a server has successfully been spawned, this is the amount of time we 455 | # wait before assuming that the server is unable to accept connections. 456 | #c.Spawner.http_timeout = 30 457 | 458 | ## The IP address (or hostname) the single-user server should listen on. 459 | # 460 | # The JupyterHub proxy implementation should be able to send packets to this 461 | # interface. 462 | #c.Spawner.ip = '' 463 | 464 | ## Minimum number of bytes a single-user notebook server is guaranteed to have 465 | # available. 466 | # 467 | # Allows the following suffixes: 468 | # - K -> Kilobytes 469 | # - M -> Megabytes 470 | # - G -> Gigabytes 471 | # - T -> Terabytes 472 | # 473 | # This needs to be supported by your spawner for it to work. 474 | #c.Spawner.mem_guarantee = None 475 | 476 | ## Maximum number of bytes a single-user notebook server is allowed to use. 477 | # 478 | # Allows the following suffixes: 479 | # - K -> Kilobytes 480 | # - M -> Megabytes 481 | # - G -> Gigabytes 482 | # - T -> Terabytes 483 | # 484 | # If the single user server tries to allocate more memory than this, it will 485 | # fail. There is no guarantee that the single-user notebook server will be able 486 | # to allocate this much memory - only that it can not allocate more than this. 487 | # 488 | # This needs to be supported by your spawner for it to work. 489 | #c.Spawner.mem_limit = None 490 | 491 | ## Path to the notebook directory for the single-user server. 492 | # 493 | # The user sees a file listing of this directory when the notebook interface is 494 | # started. The current interface does not easily allow browsing beyond the 495 | # subdirectories in this directory's tree. 496 | # 497 | # `~` will be expanded to the home directory of the user, and {username} will be 498 | # replaced with the name of the user. 499 | # 500 | # Note that this does *not* prevent users from accessing files outside of this 501 | # path! They can do so with many other means. 502 | c.Spawner.notebook_dir = '/opt/ros_hadoop/latest/doc/' 503 | 504 | ## An HTML form for options a user can specify on launching their server. 505 | # 506 | # The surrounding `
` element and the submit button are already provided. 507 | # 508 | # For example: 509 | # 510 | # .. code:: html 511 | # 512 | # Set your key: 513 | # 514 | #
515 | # Choose a letter: 516 | # 520 | # 521 | # The data from this form submission will be passed on to your spawner in 522 | # `self.user_options` 523 | #c.Spawner.options_form = '' 524 | 525 | ## Interval (in seconds) on which to poll the spawner for single-user server's 526 | # status. 527 | # 528 | # At every poll interval, each spawner's `.poll` method is called, which checks 529 | # if the single-user server is still running. If it isn't running, then 530 | # JupyterHub modifies its own state accordingly and removes appropriate routes 531 | # from the configurable proxy. 532 | #c.Spawner.poll_interval = 30 533 | 534 | ## The port for single-user servers to listen on. 535 | # 536 | # Defaults to `0`, which uses a randomly allocated port number each time. 537 | # 538 | # If set to a non-zero value, all Spawners will use the same port, which only 539 | # makes sense if each server is on a different address, e.g. in containers. 540 | # 541 | # New in version 0.7. 542 | #c.Spawner.port = 0 543 | 544 | ## An optional hook function that you can implement to do some bootstrapping work 545 | # before the spawner starts. For example, create a directory for your user or 546 | # load initial content. 547 | # 548 | # This can be set independent of any concrete spawner implementation. 549 | # 550 | # Example:: 551 | # 552 | # from subprocess import check_call 553 | # def my_hook(spawner): 554 | # username = spawner.user.name 555 | # check_call(['./examples/bootstrap-script/bootstrap.sh', username]) 556 | # 557 | # c.Spawner.pre_spawn_hook = my_hook 558 | #c.Spawner.pre_spawn_hook = None 559 | 560 | ## Timeout (in seconds) before giving up on starting of single-user server. 561 | # 562 | # This is the timeout for start to return, not the timeout for the server to 563 | # respond. Callers of spawner.start will assume that startup has failed if it 564 | # takes longer than this. start should return when the server process is started 565 | # and its location is known. 566 | #c.Spawner.start_timeout = 60 567 | 568 | #------------------------------------------------------------------------------ 569 | # LocalProcessSpawner(Spawner) configuration 570 | #------------------------------------------------------------------------------ 571 | 572 | ## A Spawner that uses `subprocess.Popen` to start single-user servers as local 573 | # processes. 574 | # 575 | # Requires local UNIX users matching the authenticated users to exist. Does not 576 | # work on Windows. 577 | # 578 | # This is the default spawner for JupyterHub. 579 | 580 | ## Seconds to wait for single-user server process to halt after SIGINT. 581 | # 582 | # If the process has not exited cleanly after this many seconds, a SIGTERM is 583 | # sent. 584 | #c.LocalProcessSpawner.interrupt_timeout = 10 585 | 586 | ## Seconds to wait for process to halt after SIGKILL before giving up. 587 | # 588 | # If the process does not exit cleanly after this many seconds of SIGKILL, it 589 | # becomes a zombie process. The hub process will log a warning and then give up. 590 | #c.LocalProcessSpawner.kill_timeout = 5 591 | 592 | ## Extra keyword arguments to pass to Popen 593 | # 594 | # when spawning single-user servers. 595 | # 596 | # For example:: 597 | # 598 | # popen_kwargs = dict(shell=True) 599 | #c.LocalProcessSpawner.popen_kwargs = {} 600 | 601 | ## Seconds to wait for single-user server process to halt after SIGTERM. 602 | # 603 | # If the process does not exit cleanly after this many seconds of SIGTERM, a 604 | # SIGKILL is sent. 605 | #c.LocalProcessSpawner.term_timeout = 5 606 | 607 | #------------------------------------------------------------------------------ 608 | # Authenticator(LoggingConfigurable) configuration 609 | #------------------------------------------------------------------------------ 610 | 611 | ## Base class for implementing an authentication provider for JupyterHub 612 | 613 | ## Set of users that will have admin rights on this JupyterHub. 614 | # 615 | # Admin users have extra privileges: 616 | # - Use the admin panel to see list of users logged in 617 | # - Add / remove users in some authenticators 618 | # - Restart / halt the hub 619 | # - Start / stop users' single-user servers 620 | # - Can access each individual users' single-user server (if configured) 621 | # 622 | # Admin access should be treated the same way root access is. 623 | # 624 | # Defaults to an empty set, in which case no user has admin access. 625 | #c.Authenticator.admin_users = set() 626 | 627 | ## Automatically begin the login process 628 | # 629 | # rather than starting with a "Login with..." link at `/hub/login` 630 | # 631 | # To work, `.login_url()` must give a URL other than the default `/hub/login`, 632 | # such as an oauth handler or another automatic login handler, registered with 633 | # `.get_handlers()`. 634 | # 635 | # .. versionadded:: 0.8 636 | #c.Authenticator.auto_login = False 637 | 638 | ## Enable persisting auth_state (if available). 639 | # 640 | # auth_state will be encrypted and stored in the Hub's database. This can 641 | # include things like authentication tokens, etc. to be passed to Spawners as 642 | # environment variables. 643 | # 644 | # Encrypting auth_state requires the cryptography package. 645 | # 646 | # Additionally, the JUPYTERHUB_CRYPTO_KEY envirionment variable must contain one 647 | # (or more, separated by ;) 32B encryption keys. These can be either base64 or 648 | # hex-encoded. 649 | # 650 | # If encryption is unavailable, auth_state cannot be persisted. 651 | # 652 | # New in JupyterHub 0.8 653 | #c.Authenticator.enable_auth_state = False 654 | 655 | ## Dictionary mapping authenticator usernames to JupyterHub users. 656 | # 657 | # Primarily used to normalize OAuth user names to local users. 658 | #c.Authenticator.username_map = {} 659 | 660 | ## Regular expression pattern that all valid usernames must match. 661 | # 662 | # If a username does not match the pattern specified here, authentication will 663 | # not be attempted. 664 | # 665 | # If not set, allow any username. 666 | #c.Authenticator.username_pattern = '' 667 | 668 | ## Whitelist of usernames that are allowed to log in. 669 | # 670 | # Use this with supported authenticators to restrict which users can log in. 671 | # This is an additional whitelist that further restricts users, beyond whatever 672 | # restrictions the authenticator has in place. 673 | # 674 | # If empty, does not perform any additional restriction. 675 | #c.Authenticator.whitelist = set() 676 | 677 | #------------------------------------------------------------------------------ 678 | # LocalAuthenticator(Authenticator) configuration 679 | #------------------------------------------------------------------------------ 680 | 681 | ## Base class for Authenticators that work with local Linux/UNIX users 682 | # 683 | # Checks for local users, and can attempt to create them if they exist. 684 | 685 | ## The command to use for creating users as a list of strings 686 | # 687 | # For each element in the list, the string USERNAME will be replaced with the 688 | # user's username. The username will also be appended as the final argument. 689 | # 690 | # For Linux, the default value is: 691 | # 692 | # ['adduser', '-q', '--gecos', '""', '--disabled-password'] 693 | # 694 | # To specify a custom home directory, set this to: 695 | # 696 | # ['adduser', '-q', '--gecos', '""', '--home', '/customhome/USERNAME', '-- 697 | # disabled-password'] 698 | # 699 | # This will run the command: 700 | # 701 | # adduser -q --gecos "" --home /customhome/river --disabled-password river 702 | # 703 | # when the user 'river' is created. 704 | #c.LocalAuthenticator.add_user_cmd = [] 705 | 706 | ## If set to True, will attempt to create local system users if they do not exist 707 | # already. 708 | # 709 | # Supports Linux and BSD variants only. 710 | #c.LocalAuthenticator.create_system_users = False 711 | 712 | ## Whitelist all users from this UNIX group. 713 | # 714 | # This makes the username whitelist ineffective. 715 | #c.LocalAuthenticator.group_whitelist = set() 716 | 717 | #------------------------------------------------------------------------------ 718 | # PAMAuthenticator(LocalAuthenticator) configuration 719 | #------------------------------------------------------------------------------ 720 | 721 | ## Authenticate local UNIX users with PAM 722 | 723 | ## The text encoding to use when communicating with PAM 724 | #c.PAMAuthenticator.encoding = 'utf8' 725 | 726 | ## Whether to open a new PAM session when spawners are started. 727 | # 728 | # This may trigger things like mounting shared filsystems, loading credentials, 729 | # etc. depending on system configuration, but it does not always work. 730 | # 731 | # If any errors are encountered when opening/closing PAM sessions, this is 732 | # automatically set to False. 733 | #c.PAMAuthenticator.open_sessions = True 734 | 735 | ## The name of the PAM service to use for authentication 736 | #c.PAMAuthenticator.service = 'login' 737 | 738 | #------------------------------------------------------------------------------ 739 | # CryptKeeper(SingletonConfigurable) configuration 740 | #------------------------------------------------------------------------------ 741 | 742 | ## Encapsulate encryption configuration 743 | # 744 | # Use via the encryption_config singleton below. 745 | 746 | ## 747 | #c.CryptKeeper.keys = [] 748 | 749 | ## The number of threads to allocate for encryption 750 | #c.CryptKeeper.n_threads = 4 751 | -------------------------------------------------------------------------------- /deploy/docker/flux/lib/protobuf-java-3.3.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flux-project/flux/0e48aaff31b0ee626e3a2ae507af953658dbcd85/deploy/docker/flux/lib/protobuf-java-3.3.0.jar -------------------------------------------------------------------------------- /deploy/docker/flux/lib/rosbaginputformat.jar: -------------------------------------------------------------------------------- 1 | rosbaginputformat_2.11-0.9.8.jar -------------------------------------------------------------------------------- /deploy/docker/flux/lib/rosbaginputformat_2.11-0.9.8.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flux-project/flux/0e48aaff31b0ee626e3a2ae507af953658dbcd85/deploy/docker/flux/lib/rosbaginputformat_2.11-0.9.8.jar -------------------------------------------------------------------------------- /deploy/docker/flux/lib/scala-library-2.11.8.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flux-project/flux/0e48aaff31b0ee626e3a2ae507af953658dbcd85/deploy/docker/flux/lib/scala-library-2.11.8.jar -------------------------------------------------------------------------------- /deploy/docker/flux/spark-ex-kubernetes.sh: -------------------------------------------------------------------------------- 1 | ## 2 | ## 3 | 4 | # Here is the working version on MyMac 5 | 6 | bin/spark-submit \ 7 | --master k8s://https://192.168.1.40:6443 \ 8 | --deploy-mode cluster \ 9 | --name spark-pi \ 10 | --class org.apache.spark.examples.SparkPi \ 11 | --conf spark.executor.instances=1 \ 12 | --conf spark.kubernetes.container.image=seunghan/spark_k8s/spark:test_0.1 \ 13 | local:///opt/spark/examples/jars/spark-examples_2.11-2.3.0.jar 14 | 15 | # k8s://https://: 16 | #spark-submit \ 17 | # --master k8s://https://192.168.1.40:6443 \ 18 | # --deploy-mode cluster \ 19 | # --name spark-pi \ 20 | # --class org.apache.spark.examples.SparkPi \ 21 | # --jars https://path/to/dependency1.jar,https://path/to/dependency2.jar 22 | # --files hdfs://host:port/path/to/file1,hdfs://host:port/path/to/file2 23 | # --conf spark.executor.instances=5 \ 24 | # --conf spark.kubernetes.container.image= \ 25 | # https://path/to/examples.jar 26 | 27 | # https://apache-spark-on-k8s.github.io/userdocs/running-on-kubernetes.html 28 | #bin/spark-submit \ 29 | # --deploy-mode cluster \ 30 | # --class org.apache.spark.examples.SparkPi \ 31 | # --master k8s://https://: \ 32 | # --kubernetes-namespace default \ 33 | # --conf spark.executor.instances=5 \ 34 | # --conf spark.app.name=spark-pi \ 35 | # --conf spark.kubernetes.driver.docker.image=kubespark/spark-driver:v2.2.0-kubernetes-0.5.0 \ 36 | # --conf spark.kubernetes.executor.docker.image=kubespark/spark-executor:v2.2.0-kubernetes-0.5.0 \ 37 | # local:///opt/spark/examples/jars/spark-examples_2.11-2.2.0-k8s-0.5.0.jar 38 | 39 | spark-submit \ 40 | --master k8s://https://192.168.1.40:6443 \ 41 | --deploy-mode cluster \ 42 | --name spark-pi \ 43 | --class org.apache.spark.examples.SparkPi \ 44 | --conf spark.executor.instances=5 \ 45 | --conf spark.kubernetes.container.image=anantpukale/spark_app:1.1 \ 46 | local:///home/flux/spark-2.3.0-bin-hadoop2.7/examples/jars/spark-examples_2.11-2.3.0.jar 47 | 48 | 49 | # Directly on mac 50 | # /opt/spark/ 51 | 52 | bin/spark-submit \ 53 | --master k8s://https://192.168.1.40:6443 \ 54 | --deploy-mode cluster \ 55 | --name spark-pi \ 56 | --class org.apache.spark.examples.SparkPi \ 57 | --conf spark.executor.instances=5 \ 58 | --conf spark.kubernetes.container.image=kubespark/spark-driver:v2.2.0-kubernetes-0.5.0 \ 59 | local:///opt/spark/examples/jars/spark-examples_2.11-2.3.0.jar 60 | 61 | 62 | 63 | 64 | 65 | ## following commands ... config params doesn't work.. 66 | 67 | spark-submit \ 68 | --master k8s://https://192.168.1.40:6443 \ 69 | --deploy-mode cluster \ 70 | --name spark-pi \ 71 | --class org.apache.spark.examples.SparkPi \ 72 | --conf spark.executor.instances=5 \ 73 | --conf spark.kubernetes.driver.docker.image=kubespark/spark-driver:v2.2.0-kubernetes-0.5.0 \ 74 | --conf spark.kubernetes.executor.docker.image=kubespark/spark-executor:v2.2.0-kubernetes-0.5.0 \ 75 | local:///home/flux/spark-2.3.0-bin-hadoop2.7/examples/jars/spark-examples_2.11-2.3.0.jar 76 | 77 | 78 | bin/spark-submit \ 79 | --deploy-mode cluster \ 80 | --class org.apache.spark.examples.SparkPi \ 81 | --master k8s://https://192.168.1.40:6443 \ 82 | --kubernetes-namespace default \ 83 | --conf spark.executor.instances=5 \ 84 | --conf spark.app.name=spark-pi \ 85 | --conf spark.kubernetes.driver.docker.image=kubespark/spark-driver:v2.2.0-kubernetes-0.5.0 \ 86 | --conf spark.kubernetes.executor.docker.image=kubespark/spark-executor:v2.2.0-kubernetes-0.5.0 \ 87 | local:///opt/spark/examples/jars/spark-examples_2.11-2.2.0-k8s-0.5.0.jar 88 | 89 | 90 | 91 | 92 | bin/spark-submit 93 | --master k8s://https://192.168.1.40:6443 94 | --deploy-mode cluster 95 | --name spark-pi 96 | --class org.apache.spark.examples.SparkPi 97 | --conf spark.executor.instances=5 98 | --conf spark.kubernetes.driver.docker.image=kubespark/spark-driver:v2.2.0-kubernetes-0.5.0 99 | --conf spark.kubernetes.executor.docker.image=kubespark/spark-executor:v2.2.0-kubernetes-0.5.0 100 | local:///opt/spark/examples/jars/spark-examples_2.11-2.3.0.jar 101 | 102 | 103 | 104 | 105 | spark-submit \ 106 | --master k8s://https://192.168.1.40:6443 \ 107 | --deploy-mode cluster \ 108 | --name spark-pi \ 109 | --class org.apache.spark.examples.SparkPi \ 110 | --conf spark.executor.instances=1 \ 111 | --conf spark.kubernetes.container.image=seunghan/spark_k8s/spark:test_0.1 \ 112 | local:///home/flux/spark-2.3.0-bin-hadoop2.7/examples/jars/spark-examples_2.11-2.3.0.jar 113 | 114 | 115 | spark-submit \ 116 | --master k8s://https://192.168.1.40:6443 \ 117 | --deploy-mode cluster \ 118 | --name spark-pi \ 119 | --class org.apache.spark.examples.SparkPi \ 120 | --conf spark.executor.instances=1 \ 121 | --conf spark.kubernetes.container.image=seunghan/spark_k8s/spark:test_0.1 \ 122 | local:///opt/apache/spark-2.3.0-bin-hadoop2.7/examples/jars/spark-examples_2.11-2.3.0.jar 123 | -------------------------------------------------------------------------------- /deploy/docker/hdfs4k8s/Dockerfile-datanode: -------------------------------------------------------------------------------- 1 | # Note : Original source of the following dockerfile 2 | # https://github.com/big-data-europe/docker-hadoop/blob/master/datanode/Dockerfile 3 | FROM bde2020/hadoop-base:1.1.0-hadoop2.7.1-java8 4 | 5 | HEALTHCHECK CMD curl -f http://localhost:50075/ || exit 1 6 | 7 | ENV HDFS_CONF_dfs_datanode_data_dir=file:///hadoop/dfs/data 8 | RUN mkdir -p /hadoop/dfs/data 9 | #VOLUME /hadoop/dfs/data 10 | 11 | COPY deploy/docker/hdfs4k8s/run-dn.sh /run.sh 12 | RUN chmod a+x /run.sh 13 | 14 | EXPOSE 50075 15 | 16 | CMD ["/run.sh"] -------------------------------------------------------------------------------- /deploy/docker/hdfs4k8s/Dockerfile-namenode: -------------------------------------------------------------------------------- 1 | # Note : Original source of the following dockerfile 2 | # https://github.com/big-data-europe/docker-hadoop/blob/master/namenode/Dockerfile 3 | FROM bde2020/hadoop-base:1.1.0-hadoop2.7.1-java8 4 | #MAINTAINER Ivan Ermilov 5 | 6 | HEALTHCHECK CMD curl -f http://localhost:50070/ || exit 1 7 | 8 | ENV HDFS_CONF_dfs_namenode_name_dir=file:///hadoop/dfs/name 9 | RUN mkdir -p /hadoop/dfs/name 10 | #VOLUME /hadoop/dfs/name 11 | 12 | COPY deploy/docker/hdfs4k8s/run-nn.sh /run.sh 13 | RUN chmod a+x /run.sh 14 | 15 | EXPOSE 50070 16 | 17 | CMD ["/run.sh"] -------------------------------------------------------------------------------- /deploy/docker/hdfs4k8s/run-dn.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #### 4 | # Original source of the following script 5 | # Source : https://github.com/big-data-europe/docker-hadoop/blob/master/datanode/run.sh 6 | #### 7 | 8 | datadir=`echo $HDFS_CONF_dfs_datanode_data_dir | perl -pe 's#file://##'` 9 | if [ ! -d $datadir ]; then 10 | echo "Datanode data directory not found: $datadir" 11 | exit 2 12 | fi 13 | 14 | $HADOOP_PREFIX/bin/hdfs --config $HADOOP_CONF_DIR datanode 15 | -------------------------------------------------------------------------------- /deploy/docker/hdfs4k8s/run-nn.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #### 4 | # Original source of the following script 5 | # Source : https://github.com/big-data-europe/docker-hadoop/tree/master/datanode 6 | #### 7 | namedir=`echo $HDFS_CONF_dfs_namenode_name_dir | perl -pe 's#file://##'` 8 | if [ ! -d $namedir ]; then 9 | echo "Namenode name directory not found: $namedir" 10 | exit 2 11 | fi 12 | 13 | if [ -z "$CLUSTER_NAME" ]; then 14 | echo "Cluster name not specified" 15 | exit 2 16 | fi 17 | 18 | if [ "`ls -A $namedir`" == "" ]; then 19 | echo "Formatting namenode name directory: $namedir" 20 | $HADOOP_PREFIX/bin/hdfs --config $HADOOP_CONF_DIR namenode -format $CLUSTER_NAME 21 | fi 22 | 23 | $HADOOP_PREFIX/bin/hdfs --config $HADOOP_CONF_DIR namenode 24 | -------------------------------------------------------------------------------- /deploy/docker/ros_base/Dockerfile: -------------------------------------------------------------------------------- 1 | # This is an auto generated Dockerfile for ros:ros-core 2 | # generated from docker_images/create_ros_core_image.Dockerfile.em 3 | FROM ubuntu:xenial 4 | #FROM nvidia/cuda:9.0-cudnn7-devel-ubuntu16.04 5 | #FROM nvidia/cuda:9.0-cudnn7-runtime-ubuntu16.04 6 | 7 | # install packages 8 | RUN apt-get update && apt-get install -y --no-install-recommends \ 9 | dirmngr \ 10 | gnupg2 \ 11 | && rm -rf /var/lib/apt/lists/* 12 | 13 | # setup keys 14 | RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 421C365BD9FF1F717815A3895523BAEEB01FA116 15 | 16 | # setup sources.list 17 | RUN echo "deb http://packages.ros.org/ros/ubuntu xenial main" > /etc/apt/sources.list.d/ros-latest.list 18 | 19 | # install bootstrap tools 20 | RUN apt-get update && apt-get install --no-install-recommends -y \ 21 | python-rosdep \ 22 | python-rosinstall \ 23 | python-vcstools \ 24 | && rm -rf /var/lib/apt/lists/* 25 | 26 | # setup environment 27 | ENV LANG C.UTF-8 28 | ENV LC_ALL C.UTF-8 29 | 30 | # bootstrap rosdep 31 | RUN rosdep init \ 32 | && rosdep update 33 | 34 | # install ros packages 35 | ENV ROS_DISTRO kinetic 36 | RUN apt-get update && apt-get install -y \ 37 | ros-kinetic-ros-core=1.3.2-0* \ 38 | && rm -rf /var/lib/apt/lists/* 39 | 40 | # setup entrypoint 41 | COPY ros_entrypoint.sh / 42 | 43 | ENTRYPOINT ["/ros_entrypoint.sh"] 44 | CMD ["bash"] 45 | -------------------------------------------------------------------------------- /deploy/docker/ros_base/Dockerfile-gpu: -------------------------------------------------------------------------------- 1 | # This is an auto generated Dockerfile for ros:ros-core 2 | # generated from docker_images/create_ros_core_image.Dockerfile.em 3 | #FROM ubuntu:xenial 4 | #FROM nvidia/cuda:9.0-cudnn7-devel-ubuntu16.04 5 | FROM nvidia/cuda:9.0-cudnn7-runtime-ubuntu16.04 6 | 7 | # install packages 8 | RUN apt-get update && apt-get install -y --no-install-recommends \ 9 | dirmngr \ 10 | gnupg2 \ 11 | && rm -rf /var/lib/apt/lists/* 12 | 13 | # setup keys 14 | RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 421C365BD9FF1F717815A3895523BAEEB01FA116 15 | 16 | # setup sources.list 17 | RUN echo "deb http://packages.ros.org/ros/ubuntu xenial main" > /etc/apt/sources.list.d/ros-latest.list 18 | 19 | # install bootstrap tools 20 | RUN apt-get update && apt-get install --no-install-recommends -y \ 21 | python-rosdep \ 22 | python-rosinstall \ 23 | python-vcstools \ 24 | && rm -rf /var/lib/apt/lists/* 25 | 26 | # setup environment 27 | ENV LANG C.UTF-8 28 | ENV LC_ALL C.UTF-8 29 | 30 | # bootstrap rosdep 31 | RUN rosdep init \ 32 | && rosdep update 33 | 34 | # install ros packages 35 | ENV ROS_DISTRO kinetic 36 | RUN apt-get update && apt-get install -y \ 37 | ros-kinetic-ros-core=1.3.2-0* \ 38 | && rm -rf /var/lib/apt/lists/* 39 | 40 | # setup entrypoint 41 | COPY ros_entrypoint.sh / 42 | 43 | ENTRYPOINT ["/ros_entrypoint.sh"] 44 | CMD ["bash"] 45 | -------------------------------------------------------------------------------- /deploy/docker/ros_base/README.txt: -------------------------------------------------------------------------------- 1 | 2 | ## ROS Dockerfile source 3 | 4 | https://github.com/osrf/docker_images/blob/f2b13092747c0f60cf7608369b57ea89bc01e22d/ros/kinetic/ubuntu/xenial/ros-core/Dockerfile 5 | 6 | 7 | -------------------------------------------------------------------------------- /deploy/docker/ros_base/ros_entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # setup ros environment 5 | source "/opt/ros/$ROS_DISTRO/setup.bash" 6 | exec "$@" 7 | -------------------------------------------------------------------------------- /deploy/kubernetes/distributed/README.md: -------------------------------------------------------------------------------- 1 | # Distrbuted multi-node HDFS set up on kubernetes 2 | 3 | ### Helm package 4 | To package the kubernetes components with helm one needs only the helm client. 5 | If you do not have helm already, download it and run once ```helm init -c``` (just ```helm init``` if tiller is not already installed) that will create a .helm folder structure in your $HOME 6 | 7 | Note: On a cluster using Ubuntu OS run the following 8 | ```bash 9 | kubectl patch deploy --namespace kube-system tiller-deploy -p '{"spec":{"template":{"spec":{"serviceAccount":"tiller"}}}}' 10 | ``` 11 | 12 | Lint the helm packages 13 | ```bash 14 | helm lint $(./flux) hdfs-pvc/ 15 | helm lint $(./flux) hdfs-flux/ 16 | ``` 17 | 18 | Package the helm charts as follows 19 | ```bash 20 | helm package hdfs-pvc/ 21 | helm package hdfs-flux/ 22 | ``` 23 | 24 | ### Flux namespace 25 | 26 | All components assume a kubernetes namespace flux. 27 | ```bash 28 | kubectl create namespace flux 29 | ``` 30 | 31 | ### Install HDFS 32 | 33 | Label one kubernetes node as hdfs-namenode 34 | ```bash 35 | kubectl label no hdfs-namenode-selector=hdfs-namenode 36 | ``` 37 | Label a list of kubernetes nodes as hdfs-datanode 38 | ```bash 39 | kubectl label no hdfs-datanode-selector=hdfs-datanode 40 | # ... 41 | ``` 42 | 43 | NOTE: If you need to specify persistence volume manually, install the `hdfs-pv` helm package. (By default namenode and datanode will use `/tmp/name` and `/tmp/data` on host machines. You can customize the path as specified in the below `install` command.) 44 | 45 | ```bash 46 | helm lint $(./flux) hdfs-pv/ 47 | helm package hdfs-pv/ 48 | helm install --name hdfs-pv $(./flux) --set flux.datanode_host_path="/path/to/storage/dn" --set flux.namenode_host_path="/path/to/storage/nn" hdfs-pv-0.2.0.tgz 49 | ``` 50 | 51 | Install the helm packages 52 | ```bash 53 | helm install --name=hdfs-pvc $(./flux) hdfs-pvc-0.2.0.tgz 54 | helm install --name=hdfs $(./flux) ./hdfs-flux-0.2.0.tgz 55 | ``` 56 | 57 | Connect to a datanode and create the HDFS directories 58 | ```bash 59 | kubectl exec -it -n flux datanode-0 -- hdfs dfs -mkdir -p /user/flux 60 | ``` 61 | 62 | Delete the hdfs pods and services 63 | ```bash 64 | helm delete --purge hdfs 65 | ``` 66 | 67 | Delete the hdfs PersistenceVolumeClaim 68 | ```bash 69 | helm delete --purge hdfs-pvc 70 | ``` 71 | 72 | Delete the hdfs PersistenceVolume 73 | ```bash 74 | helm delete --purge hdfs-pv 75 | ``` 76 | -------------------------------------------------------------------------------- /deploy/kubernetes/distributed/flux: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | NODES=$(kubectl get nodes -o jsonpath='{.items..metadata.name}') 4 | NODES_CNT=$(echo "${NODES}" | tr ' ' '\n' | wc -l | xargs echo -n) 5 | NAMENODES=$(kubectl get nodes -o jsonpath="{.items[?(@.metadata.labels['hdfs-namenode-selector'])]..metadata.name}" | tr ' ' ',') 6 | NAMENODES_CNT=$(echo "${NAMENODES}" | tr ',' '\n' | wc -l | xargs echo -n) 7 | DATANODES=$(kubectl get nodes -o jsonpath="{.items[?(@.metadata.labels['hdfs-datanode-selector'])]..metadata.name}" | tr ' ' ',') 8 | DATANODES_CNT=$(echo "${DATANODES}" | tr ',' '\n' | wc -l | xargs echo -n) 9 | NAMENODE_HOST_PATH='/tmp' 10 | DATANODE_HOST_PATH='/tmp' 11 | #echo "Nodes: ${NODES}" 12 | #echo "Namenodes: ${NAMENODES}" 13 | #echo "DataNodes: ${DATANODES}" 14 | #echo "Namenodes hostpath: ${NAMENODE_HOST_PATH}" 15 | #echo "Datanodes hostpath: ${DATANODE_HOST_PATH}" 16 | echo -n "--set flux.datanodes_cnt=${DATANODES_CNT} --set flux.datanodes={${DATANODES}} --set flux.namenodes={${NAMENODES}} " 17 | #--set flux.datanode.host_path=${DATANODE_HOST_PATH} --set flux.namenode_host_path=${NAMENODE_HOST_PATH} " 18 | -------------------------------------------------------------------------------- /deploy/kubernetes/distributed/flux-init: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | kubectl create ns flux 3 | -------------------------------------------------------------------------------- /deploy/kubernetes/distributed/hdfs-flux/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *~ 18 | # Various IDEs 19 | .project 20 | .idea/ 21 | *.tmproj 22 | -------------------------------------------------------------------------------- /deploy/kubernetes/distributed/hdfs-flux/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | appVersion: "1.0" 3 | description: HDFS Helm chart for Kubernetes 4 | name: hdfs-flux 5 | version: 0.2.0 6 | home: https://hadoop.apache.org/ 7 | sources: 8 | - https://github.com/apache/hadoop 9 | icon: http://hadoop.apache.org/images/hadoop-logo.jpg 10 | -------------------------------------------------------------------------------- /deploy/kubernetes/distributed/hdfs-flux/templates/NOTES.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flux-project/flux/0e48aaff31b0ee626e3a2ae507af953658dbcd85/deploy/kubernetes/distributed/hdfs-flux/templates/NOTES.txt -------------------------------------------------------------------------------- /deploy/kubernetes/distributed/hdfs-flux/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "hdfs-flux.name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | If release name contains chart name it will be used as a full name. 13 | */}} 14 | {{- define "hdfs-flux.fullname" -}} 15 | {{- if .Values.fullnameOverride -}} 16 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} 17 | {{- else -}} 18 | {{- $name := default .Chart.Name .Values.nameOverride -}} 19 | {{- if contains $name .Release.Name -}} 20 | {{- .Release.Name | trunc 63 | trimSuffix "-" -}} 21 | {{- else -}} 22 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 23 | {{- end -}} 24 | {{- end -}} 25 | {{- end -}} 26 | 27 | {{/* 28 | Create chart name and version as used by the chart label. 29 | */}} 30 | {{- define "hdfs-flux.chart" -}} 31 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} 32 | {{- end -}} 33 | -------------------------------------------------------------------------------- /deploy/kubernetes/distributed/hdfs-flux/templates/dn-ds.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: datanode 5 | namespace: flux 6 | spec: 7 | ports: 8 | - name: dfs 9 | port: 50020 10 | protocol: TCP 11 | - name: datatransfer 12 | port: 50010 13 | protocol: TCP 14 | - name: webhdfs 15 | port: 50075 16 | clusterIP: None 17 | selector: 18 | name: datanode 19 | app: flux-dn 20 | 21 | --- 22 | apiVersion: apps/v1 23 | kind: StatefulSet 24 | metadata: 25 | name: datanode 26 | namespace: flux 27 | spec: 28 | selector: 29 | matchLabels: 30 | name: datanode 31 | app: flux-dn 32 | serviceName: datanode 33 | replicas: {{ .Values.flux.datanodes_cnt }} 34 | template: 35 | metadata: 36 | labels: 37 | name: datanode 38 | app: flux-dn 39 | spec: 40 | dnsPolicy: ClusterFirstWithHostNet 41 | containers: 42 | - name: datanode 43 | image: {{ .Values.image.datanode }} 44 | hostname: datanode 45 | ports: 46 | - containerPort: 50075 47 | - containerPort: 50010 48 | - containerPort: 50020 49 | envFrom: 50 | - configMapRef: 51 | name: hadoop-cm 52 | volumeMounts: 53 | - name: datanode-volume 54 | mountPath: /hadoop/dfs 55 | nodeSelector: 56 | hdfs-datanode-selector: hdfs-datanode 57 | volumeClaimTemplates: 58 | - metadata: 59 | name: datanode-volume 60 | spec: 61 | accessModes: 62 | - ReadWriteOnce 63 | selector: 64 | matchLabels: 65 | datanode-pv-label: pvc-hdfs-dn 66 | 67 | -------------------------------------------------------------------------------- /deploy/kubernetes/distributed/hdfs-flux/templates/hdfs-cm.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | data: 3 | CORE_CONF_fs_defaultFS: hdfs://namenode:8020 4 | CORE_CONF_hadoop_http_staticuser_user: root 5 | CORE_CONF_hadoop_proxyuser_hue_groups: '*' 6 | CORE_CONF_hadoop_proxyuser_hue_hosts: '*' 7 | HDFS_CONF_dfs_permissions_enabled: "false" 8 | HDFS_CONF_dfs_webhdfs_enabled: "true" 9 | HDFS_CONF_dfs_namenode_datanode_registration_ip___hostname___check: "false" 10 | kind: ConfigMap 11 | metadata: 12 | name: hadoop-cm 13 | namespace: flux 14 | 15 | -------------------------------------------------------------------------------- /deploy/kubernetes/distributed/hdfs-flux/templates/nn-pod.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: namenode 5 | namespace: flux 6 | labels: 7 | app: flux-nn 8 | chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_"}} 9 | release: {{ .Release.Name }} 10 | heritage: {{ .Release.Service }} 11 | spec: 12 | hostNetwork: false 13 | hostPID: true 14 | dnsPolicy: ClusterFirstWithHostNet 15 | containers: 16 | - name: namenode 17 | replicas: 1 18 | image: {{ .Values.image.namenode }} 19 | hostname: namenode 20 | ports: 21 | - containerPort: 50070 22 | - containerPort: 8020 23 | env: 24 | - name: CLUSTER_NAME 25 | value: flux-cluster 26 | envFrom: 27 | - configMapRef: 28 | name: hadoop-cm 29 | volumeMounts: 30 | - name: namenode-volume 31 | mountPath: /hadoop/dfs 32 | volumes: 33 | - name: namenode-volume 34 | persistentVolumeClaim: 35 | claimName: namenode-volume-namenode-0 36 | nodeSelector: 37 | hdfs-namenode-selector: hdfs-namenode 38 | 39 | -------------------------------------------------------------------------------- /deploy/kubernetes/distributed/hdfs-flux/templates/nn-svc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: namenode 5 | namespace: flux 6 | labels: 7 | app: flux-nn 8 | chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_"}} 9 | release: {{ .Release.Name }} 10 | heritage: {{ .Release.Service }} 11 | spec: 12 | ports: 13 | - protocol: TCP 14 | port: 8020 15 | name: namenode-port 16 | - protocol: TCP 17 | port: 50070 18 | name: namenode-port-ui 19 | clusterIP: None 20 | selector: 21 | app: flux-nn 22 | release: {{ .Release.Name }} 23 | -------------------------------------------------------------------------------- /deploy/kubernetes/distributed/hdfs-flux/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for hdfs-flux. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | replicaCount: 1 6 | 7 | image: 8 | datanode: bde2020/hadoop-datanode:1.1.0-hadoop2.8-java8 9 | namenode: bde2020/hadoop-namenode:1.1.0-hadoop2.8-java8 10 | 11 | persistence: 12 | datanode: 13 | accessMode: ReadWriteOnce 14 | size: 10Gi 15 | namenode: 16 | accessMode: ReadWriteOnce 17 | size: 10Gi 18 | 19 | -------------------------------------------------------------------------------- /deploy/kubernetes/distributed/hdfs-pv/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *~ 18 | # Various IDEs 19 | .project 20 | .idea/ 21 | *.tmproj 22 | -------------------------------------------------------------------------------- /deploy/kubernetes/distributed/hdfs-pv/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | appVersion: "1.0" 3 | description: HDFS PV Helm chart for Kubernetes 4 | name: hdfs-pv 5 | version: 0.2.0 6 | home: https://github.com/flux-project/flux 7 | sources: 8 | - https://github.com/flux-project/flux 9 | icon: http://hadoop.apache.org/images/hadoop-logo.jpg 10 | -------------------------------------------------------------------------------- /deploy/kubernetes/distributed/hdfs-pv/templates/NOTES.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flux-project/flux/0e48aaff31b0ee626e3a2ae507af953658dbcd85/deploy/kubernetes/distributed/hdfs-pv/templates/NOTES.txt -------------------------------------------------------------------------------- /deploy/kubernetes/distributed/hdfs-pv/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "hdfs-pv.name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | If release name contains chart name it will be used as a full name. 13 | */}} 14 | {{- define "hdfs-pv.fullname" -}} 15 | {{- if .Values.fullnameOverride -}} 16 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} 17 | {{- else -}} 18 | {{- $name := default .Chart.Name .Values.nameOverride -}} 19 | {{- if contains $name .Release.Name -}} 20 | {{- .Release.Name | trunc 63 | trimSuffix "-" -}} 21 | {{- else -}} 22 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 23 | {{- end -}} 24 | {{- end -}} 25 | {{- end -}} 26 | 27 | {{/* 28 | Create chart name and version as used by the chart label. 29 | */}} 30 | {{- define "hdfs-pv.chart" -}} 31 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} 32 | {{- end -}} 33 | -------------------------------------------------------------------------------- /deploy/kubernetes/distributed/hdfs-pv/templates/dn-pv.yaml: -------------------------------------------------------------------------------- 1 | {{- range $i,$dn := .Values.flux.datanodes }} 2 | --- 3 | kind: PersistentVolume 4 | apiVersion: v1 5 | metadata: 6 | name: datanode-volume-datanode-{{ $i }} 7 | labels: 8 | type: local 9 | spec: 10 | capacity: 11 | storage: {{ $.Values.persistence.datanode.size |quote }} 12 | accessModes: 13 | - {{ $.Values.persistence.datanode.accessMode | quote }} 14 | hostPath: 15 | path: {{ $.Values.flux.datanode_host_path | default "/tmp/data" | quote }} 16 | {{- end}} 17 | -------------------------------------------------------------------------------- /deploy/kubernetes/distributed/hdfs-pv/templates/nn-pv.yaml: -------------------------------------------------------------------------------- 1 | kind: PersistentVolume 2 | apiVersion: v1 3 | metadata: 4 | name: namenode-volume-namenode-0 5 | labels: 6 | type: local 7 | spec: 8 | capacity: 9 | storage: {{ .Values.persistence.namenode.size |quote }} 10 | accessModes: 11 | - {{ .Values.persistence.namenode.accessMode | quote }} 12 | hostPath: 13 | path: {{ .Values.flux.namenode_host_path | default "/tmp/name" | quote }} 14 | 15 | 16 | -------------------------------------------------------------------------------- /deploy/kubernetes/distributed/hdfs-pv/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for hdfs-pv flux. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | replicaCount: 1 6 | 7 | persistence: 8 | datanode: 9 | accessMode: ReadWriteOnce 10 | size: 10Gi 11 | namenode: 12 | accessMode: ReadWriteOnce 13 | size: 10Gi 14 | 15 | -------------------------------------------------------------------------------- /deploy/kubernetes/distributed/hdfs-pvc/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | appVersion: "1.0" 3 | description: HDFS Helm chart for Kubernetes 4 | name: hdfs-pvc 5 | version: 0.2.0 6 | home: https://github.com/flux-project/ 7 | sources: 8 | - https://github.com/flux-project/ 9 | icon: http://hadoop.apache.org/images/hadoop-logo.jpg 10 | -------------------------------------------------------------------------------- /deploy/kubernetes/distributed/hdfs-pvc/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "hdfs-flux.name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | If release name contains chart name it will be used as a full name. 13 | */}} 14 | {{- define "hdfs-flux.fullname" -}} 15 | {{- if .Values.fullnameOverride -}} 16 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} 17 | {{- else -}} 18 | {{- $name := default .Chart.Name .Values.nameOverride -}} 19 | {{- if contains $name .Release.Name -}} 20 | {{- .Release.Name | trunc 63 | trimSuffix "-" -}} 21 | {{- else -}} 22 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 23 | {{- end -}} 24 | {{- end -}} 25 | {{- end -}} 26 | 27 | {{/* 28 | Create chart name and version as used by the chart label. 29 | */}} 30 | {{- define "hdfs-flux.chart" -}} 31 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} 32 | {{- end -}} 33 | -------------------------------------------------------------------------------- /deploy/kubernetes/distributed/hdfs-pvc/templates/dn-pvc.yaml: -------------------------------------------------------------------------------- 1 | {{- range $i,$dn := .Values.flux.datanodes }} 2 | --- 3 | apiVersion: v1 4 | kind: PersistentVolumeClaim 5 | metadata: 6 | name: datanode-volume-datanode-{{ $i }} 7 | namespace: flux 8 | labels: 9 | datanode-pv-label: pvc-hdfs-dn 10 | app: flux-dn-pvc 11 | chart: {{ $.Chart.Name }}-{{ $.Chart.Version | replace "+" "_"}} 12 | release: {{ $.Release.Name }} 13 | heritage: {{ $.Release.Service }} 14 | spec: 15 | accessModes: 16 | - {{ $.Values.persistence.datanode.accessMode | quote }} 17 | resources: 18 | requests: 19 | storage: {{ $.Values.persistence.datanode.size |quote }} 20 | {{- end }} 21 | -------------------------------------------------------------------------------- /deploy/kubernetes/distributed/hdfs-pvc/templates/nn-pvc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolumeClaim 3 | metadata: 4 | name: namenode-volume-namenode-0 5 | namespace: flux 6 | labels: 7 | app: flux-nn 8 | chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_"}} 9 | release: {{ .Release.Name }} 10 | heritage: {{ .Release.Service }} 11 | spec: 12 | accessModes: 13 | - {{ .Values.persistence.namenode.accessMode | quote }} 14 | resources: 15 | requests: 16 | storage: {{ .Values.persistence.namenode.size |quote }} 17 | 18 | -------------------------------------------------------------------------------- /deploy/kubernetes/distributed/hdfs-pvc/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for hdfs-flux. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | replicaCount: 1 6 | 7 | image: 8 | datanode: fluxproject/hdfs-dn-4k8s:0.1 9 | namenode: fluxproject/hdfs-nn-4k8s:0.1 10 | 11 | persistence: 12 | datanode: 13 | accessMode: ReadWriteOnce 14 | size: 10Gi 15 | namenode: 16 | accessMode: ReadWriteOnce 17 | size: 10Gi 18 | 19 | -------------------------------------------------------------------------------- /deploy/kubernetes/flux-ros-hadoop-deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Deployment 3 | metadata: 4 | annotations: 5 | deployment.kubernetes.io/revision: "1" 6 | creationTimestamp: null 7 | generation: 1 8 | labels: 9 | run: flux-ros-hadoop 10 | name: flux-ros-hadoop 11 | selfLink: /apis/extensions/v1beta1/namespaces/default/deployments/flux-ros-hadoop 12 | spec: 13 | replicas: 1 14 | selector: 15 | matchLabels: 16 | run: flux-ros-hadoop 17 | strategy: 18 | rollingUpdate: 19 | maxSurge: 1 20 | maxUnavailable: 1 21 | type: RollingUpdate 22 | template: 23 | metadata: 24 | creationTimestamp: null 25 | labels: 26 | run: flux-ros-hadoop 27 | spec: 28 | containers: 29 | - image: fluxproject/examples 30 | imagePullPolicy: IfNotPresent 31 | name: flux-ros-hadoop 32 | ports: 33 | - containerPort: 8000 34 | protocol: TCP 35 | terminationMessagePath: /dev/termination-log 36 | terminationMessagePolicy: File 37 | dnsPolicy: ClusterFirst 38 | restartPolicy: Always 39 | schedulerName: default-scheduler 40 | securityContext: {} 41 | terminationGracePeriodSeconds: 30 42 | status: {} 43 | -------------------------------------------------------------------------------- /deploy/kubernetes/flux-ros-hadoop-gpu-deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Deployment 3 | metadata: 4 | annotations: 5 | deployment.kubernetes.io/revision: "1" 6 | creationTimestamp: null 7 | generation: 1 8 | labels: 9 | run: flux-ros-hadoop-gpu 10 | name: flux-ros-hadoop-gpu 11 | selfLink: /apis/extensions/v1beta1/namespaces/default/deployments/flux-ros-hadoop-gpu 12 | spec: 13 | replicas: 1 14 | selector: 15 | matchLabels: 16 | run: flux-ros-hadoop-gpu 17 | strategy: 18 | rollingUpdate: 19 | maxSurge: 1 20 | maxUnavailable: 1 21 | type: RollingUpdate 22 | template: 23 | metadata: 24 | creationTimestamp: null 25 | labels: 26 | run: flux-ros-hadoop-gpu 27 | spec: 28 | containers: 29 | - image: fluxproject/examples_gpu 30 | imagePullPolicy: IfNotPresent 31 | name: flux-ros-hadoop-gpu 32 | ports: 33 | - containerPort: 8000 34 | protocol: TCP 35 | resources: 36 | limits: 37 | nvidia.com/gpu: 1 38 | requests: 39 | nvidia.com/gpu: 1 40 | terminationMessagePath: /dev/termination-log 41 | terminationMessagePolicy: File 42 | dnsPolicy: ClusterFirst 43 | restartPolicy: Always 44 | schedulerName: default-scheduler 45 | securityContext: {} 46 | terminationGracePeriodSeconds: 30 47 | status: {} 48 | -------------------------------------------------------------------------------- /deploy/kubernetes/flux-ros-hadoop-gpu-service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | creationTimestamp: null 5 | labels: 6 | run: flux-ros-hadoop-gpu 7 | name: flux-ros-hadoop-gpu 8 | selfLink: /api/v1/namespaces/default/services/flux-ros-hadoop-gpu 9 | spec: 10 | externalTrafficPolicy: Cluster 11 | ports: 12 | - port: 8000 13 | protocol: TCP 14 | targetPort: 8000 15 | selector: 16 | run: flux-ros-hadoop-gpu 17 | sessionAffinity: None 18 | type: NodePort 19 | status: 20 | loadBalancer: {} 21 | -------------------------------------------------------------------------------- /deploy/kubernetes/flux-ros-hadoop-service.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | creationTimestamp: null 5 | labels: 6 | run: flux-ros-hadoop 7 | name: flux-ros-hadoop 8 | selfLink: /api/v1/namespaces/default/services/flux-ros-hadoop 9 | spec: 10 | externalTrafficPolicy: Cluster 11 | ports: 12 | - port: 8000 13 | protocol: TCP 14 | targetPort: 8000 15 | selector: 16 | run: flux-ros-hadoop 17 | sessionAffinity: None 18 | type: NodePort 19 | status: 20 | loadBalancer: {} 21 | -------------------------------------------------------------------------------- /examples/concept.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flux-project/flux/0e48aaff31b0ee626e3a2ae507af953658dbcd85/examples/concept.png -------------------------------------------------------------------------------- /examples/drive-obj-detect.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flux-project/flux/0e48aaff31b0ee626e3a2ae507af953658dbcd85/examples/drive-obj-detect.mp4 -------------------------------------------------------------------------------- /examples/drive-stats.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flux-project/flux/0e48aaff31b0ee626e3a2ae507af953658dbcd85/examples/drive-stats.mp4 -------------------------------------------------------------------------------- /examples/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flux-project/flux/0e48aaff31b0ee626e3a2ae507af953658dbcd85/examples/header.png -------------------------------------------------------------------------------- /examples/lane_detector.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import cv2 3 | from PIL import Image 4 | import glob 5 | from line import Line 6 | 7 | class LaneDetector: 8 | def __init__(self, sample_img_path): 9 | self.nx, self.ny = 9, 6 10 | self.sample_img = cv2.imread(sample_img_path) 11 | self.img_size = self.sample_img.shape[1::-1] 12 | self.mtx, self.dist = self.calibrate_camera() 13 | self.s_thresh=(170, 255) 14 | self.sx_thresh=(20, 100) 15 | self.img_src_points, self.warped_img, self.perspective_M, self.Minv = self.corners_unwarp(self.sample_img, self.mtx, self.dist) 16 | self.ploty = np.linspace(0, self.sample_img.shape[0]-1, num=self.sample_img.shape[0]) 17 | self.y_eval = np.max(self.ploty) 18 | # window settings 19 | self.window_width = 50 20 | self.window_height = 80 # Break image into 9 vertical layers since image height is 720 21 | self.margin = 100 22 | self.ym_per_pix = 30/720 23 | self.xm_per_pix = 3.7/700 24 | self.left_lane = Line() 25 | self.right_lane = Line() 26 | self.last_n_frames = 1 27 | 28 | def calibrate_camera(self): 29 | objp = np.zeros((self.ny*self.nx,3), np.float32) 30 | objp[:,:2] = np.mgrid[0:self.nx, 0:self.ny].T.reshape(-1,2) 31 | 32 | objpoints = [] 33 | imgpoints = [] 34 | cal_images_path = "./camera_cal/*jpg" 35 | cal_images = glob.glob(cal_images_path) 36 | 37 | for idx, fname in enumerate(cal_images): 38 | img = cv2.imread(fname) 39 | gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) 40 | 41 | # Find the chessboard corners 42 | ret, corners = cv2.findChessboardCorners(gray, (self.nx, self.ny), None) 43 | 44 | # If found, add object points, image points 45 | if ret == True: 46 | objpoints.append(objp) 47 | imgpoints.append(corners) 48 | 49 | cal_img = cv2.imread('camera_cal/calibration1.jpg') 50 | cal_img_size = (cal_img.shape[1::-1]) 51 | ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, cal_img_size, None, None) 52 | 53 | return mtx, dist 54 | 55 | def thresholding_pipeline(self, img, s_thresh=(170, 255), sx_thresh=(20, 100)): 56 | img = np.copy(img) 57 | # Convert to HLS color space 58 | hls = cv2.cvtColor(img, cv2.COLOR_BGR2HLS) 59 | h_channel = hls[:,:,0] 60 | l_channel = hls[:,:,1] 61 | s_channel = hls[:,:,2] 62 | 63 | gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) 64 | # Sobel x 65 | sobelx = cv2.Sobel(gray, cv2.CV_64F, 1, 0) # Take the derivative in x 66 | abs_sobelx = np.absolute(sobelx) # Absolute x derivative to accentuate lines away from horizontal 67 | scaled_sobel = np.uint8(255*abs_sobelx/np.max(abs_sobelx)) 68 | 69 | # Threshold x gradient 70 | sxbinary = np.zeros_like(scaled_sobel) 71 | sxbinary[(scaled_sobel >= sx_thresh[0]) & (scaled_sobel <= sx_thresh[1])] = 1 72 | 73 | # Threshold color channel 74 | s_binary = np.zeros_like(s_channel) 75 | s_binary[(s_channel >= s_thresh[0]) & (s_channel <= s_thresh[1])] = 1 76 | # Stack each channel 77 | color_binary = np.dstack(( np.zeros_like(sxbinary), sxbinary, s_binary)) * 255 78 | 79 | combined_binary = np.zeros_like(sxbinary) 80 | combined_binary[(s_binary == 1) | (sxbinary == 1)] = 1 81 | 82 | return color_binary, combined_binary 83 | 84 | def corners_unwarp(self, img, mtx, dist): 85 | undist = cv2.undistort(img, mtx, dist, None, mtx) 86 | img_size = undist.shape[1::-1] 87 | src = np.float32([[600, 450], [685, 450], 88 | [1100, 720], [200, 720]]) 89 | 90 | dst = np.float32([[300, 0], [980, 0], 91 | [980, 720], [300, 720]]) 92 | 93 | M = cv2.getPerspectiveTransform(src, dst) 94 | Minv = cv2.getPerspectiveTransform(dst, src) 95 | warped = cv2.warpPerspective(img, M ,img_size, flags=cv2.INTER_LINEAR) 96 | 97 | cv2.polylines(img,np.int32([src]),True,(255,0,0),thickness=3) 98 | cv2.polylines(warped,np.int32([dst]),True,(255,0,0),thickness=3) 99 | 100 | return img, warped, M, Minv 101 | 102 | 103 | def window_mask(self, width, height, img_ref, center,level): 104 | output = np.zeros_like(img_ref) 105 | output[int(img_ref.shape[0]-(level+1)*height):int(img_ref.shape[0]-level*height),max(0,int(center-width/2)):min(int(center+width/2),img_ref.shape[1])] = 1 106 | return output 107 | 108 | def find_window_centroids(self, image, window_width, window_height, margin): 109 | window_centroids = [] # Store the (left,right) window centroid positions per level 110 | window = np.ones(window_width) # Create our window template that we will use for convolutions 111 | 112 | # First find the two starting positions for the left and right lane by using np.sum to get the vertical image slice 113 | # and then np.convolve the vertical image slice with the window template 114 | 115 | # Sum quarter bottom of image to get slice, could use a different ratio 116 | l_sum = np.sum(image[int(3*image.shape[0]/4):,:int(image.shape[1]/2)], axis=0) 117 | l_center = np.argmax(np.convolve(window,l_sum))-window_width/2 118 | r_sum = np.sum(image[int(3*image.shape[0]/4):,int(image.shape[1]/2):], axis=0) 119 | r_center = np.argmax(np.convolve(window,r_sum))-window_width/2+int(image.shape[1]/2) 120 | 121 | # Add what we found for the first layer 122 | window_centroids.append((l_center,r_center)) 123 | 124 | # Go through each layer looking for max pixel locations 125 | for level in range(1,(int)(image.shape[0]/window_height)): 126 | # convolve the window into the vertical slice of the image 127 | image_layer = np.sum(image[int(image.shape[0]-(level+1)*window_height):int(image.shape[0]-level*window_height),:], axis=0) 128 | conv_signal = np.convolve(window, image_layer) 129 | # Find the best left centroid by using past left center as a reference 130 | # Use window_width/2 as offset because convolution signal reference is at right side of window, not center of window 131 | offset = window_width/2 132 | l_min_index = int(max(l_center+offset-margin,0)) 133 | l_max_index = int(min(l_center+offset+margin,image.shape[1])) 134 | l_center = np.argmax(conv_signal[l_min_index:l_max_index])+l_min_index-offset 135 | # Find the best right centroid by using past right center as a reference 136 | r_min_index = int(max(r_center+offset-margin,0)) 137 | r_max_index = int(min(r_center+offset+margin,image.shape[1])) 138 | r_center = np.argmax(conv_signal[r_min_index:r_max_index])+r_min_index-offset 139 | # Add what we found for that layer 140 | window_centroids.append((l_center,r_center)) 141 | 142 | return window_centroids 143 | 144 | def detect_lane_pixles(self, binary_warped): 145 | window_centroids = self.find_window_centroids(binary_warped, self.window_width, self.window_height, self.margin) 146 | 147 | # If we found any window centers 148 | if len(window_centroids) > 0: 149 | 150 | # Points used to draw all the left and right windows 151 | l_points = np.zeros_like(binary_warped) 152 | r_points = np.zeros_like(binary_warped) 153 | 154 | # Go through each level and draw the windows 155 | for level in range(0,len(window_centroids)): 156 | # Window_mask is a function to draw window areas 157 | l_mask = self.window_mask(self.window_width,self.window_height,binary_warped,window_centroids[level][0],level) 158 | r_mask = self.window_mask(self.window_width,self.window_height,binary_warped,window_centroids[level][1],level) 159 | # Add graphic points from window mask here to total pixels found 160 | l_points[(l_points == 255) | ((l_mask == 1) ) ] = 255 161 | r_points[(r_points == 255) | ((r_mask == 1) ) ] = 255 162 | 163 | # Draw the results 164 | template = np.array(r_points+l_points,np.uint8) # add both left and right window pixels together 165 | zero_channel = np.zeros_like(template) # create a zero color channel 166 | template = np.array(cv2.merge((zero_channel,template,zero_channel)),np.uint8) # make window pixels green 167 | warpage= np.dstack((binary_warped, binary_warped, binary_warped))*255 # making the original road pixels 3 color channels 168 | output = cv2.addWeighted(warpage, 1, template, 0.5, 0.0) # overlay the orignal road image with window results 169 | 170 | # If no window centers found, just display orginal road image 171 | else: 172 | output = np.array(cv2.merge((binary_warped,binary_warped,binary_warped)),np.uint8) 173 | 174 | return output, l_points, r_points 175 | 176 | def get_lane_features(self, binary_warped): 177 | lane_lines, l_points, r_points = self.detect_lane_pixles(binary_warped) 178 | 179 | left_lane_pixels = np.nonzero(l_points) 180 | right_lane_pixels = np.nonzero(r_points) 181 | 182 | self.left_lane.detected = True 183 | self.right_lane.detected = True 184 | 185 | self.left_lane.allx = left_lane_pixels[1] 186 | self.left_lane.ally = left_lane_pixels[0] 187 | self.right_lane.allx = right_lane_pixels[1] 188 | self.right_lane.ally = right_lane_pixels[0] 189 | 190 | left_fit = np.polyfit(self.left_lane.ally, self.left_lane.allx, 2) 191 | right_fit = np.polyfit(self.right_lane.ally, self.right_lane.allx, 2) 192 | 193 | if(len(self.left_lane.current_fit) >= self.last_n_frames): 194 | self.left_lane.current_fit.pop() 195 | self.left_lane.current_fit.append(left_fit) 196 | 197 | if(len(self.right_lane.current_fit) >= self.last_n_frames): 198 | self.right_lane.current_fit.pop() 199 | self.right_lane.current_fit.append(right_fit) 200 | 201 | self.left_lane.best_fit = np.mean( self.left_lane.current_fit, axis=0 ) 202 | self.right_lane.best_fit = np.mean( self.right_lane.current_fit, axis=0 ) 203 | 204 | left_x_fitted = left_fit[0] * self.ploty**2 + left_fit[1] * self.ploty + left_fit[2] 205 | right_x_fitted = right_fit[0] * self.ploty**2 + right_fit[1] * self.ploty + right_fit[2] 206 | 207 | for i in range(len(left_x_fitted)): 208 | if (right_x_fitted[i] - left_x_fitted[i]) > 706: 209 | left_x_fitted[i] = right_x_fitted[i] - 700 210 | 211 | if(len(self.left_lane.recent_xfitted) >= self.last_n_frames): 212 | self.left_lane.recent_xfitted.pop() 213 | self.left_lane.recent_xfitted.append(left_x_fitted) 214 | 215 | if(len(self.right_lane.recent_xfitted) >= self.last_n_frames): 216 | self.right_lane.recent_xfitted.pop() 217 | self.right_lane.recent_xfitted.append(right_x_fitted) 218 | 219 | self.left_lane.bestx = np.mean( self.left_lane.recent_xfitted, axis=0 ) 220 | self.right_lane.bestx = np.mean( self.right_lane.recent_xfitted, axis=0 ) 221 | 222 | left_fit_cr = np.polyfit(self.ploty * self.ym_per_pix, self.left_lane.bestx * self.xm_per_pix, 2) 223 | right_fit_cr = np.polyfit(self.ploty * self.ym_per_pix, self.right_lane.bestx * self.xm_per_pix, 2) 224 | # Calculate the new radii of curvature 225 | self.left_lane.radius_of_curvature = ((1 + (2 * left_fit_cr[0] * self.y_eval * self.ym_per_pix + left_fit_cr[1])**2)**1.5) / np.absolute(2*left_fit_cr[0]) 226 | self.right_lane.radius_of_curvature = ((1 + (2 * right_fit_cr[0] * self.y_eval * self.ym_per_pix + right_fit_cr[1])**2)**1.5) / np.absolute(2*right_fit_cr[0]) 227 | 228 | return 229 | 230 | def draw_lane_path(self, binary, undist): 231 | warp_zero = np.zeros_like(binary).astype(np.uint8) 232 | color_warp = np.dstack((warp_zero, warp_zero, warp_zero)) 233 | 234 | # Recast the x and y points into usable format for cv2.fillPoly() 235 | pts_left = np.array([np.transpose(np.vstack([self.left_lane.bestx, self.ploty]))]) 236 | pts_right = np.array([np.flipud(np.transpose(np.vstack([self.right_lane.bestx, self.ploty])))]) 237 | pts = np.hstack((pts_left, pts_right)) 238 | 239 | # Draw the lane onto the warped blank image 240 | cv2.fillPoly(color_warp, np.int_([pts]), (0,255, 0)) 241 | 242 | # Warp the blank back to original image space using inverse perspective matrix (Minv) 243 | newwarp = cv2.warpPerspective(color_warp, self.Minv, (undist.shape[1], undist.shape[0])) 244 | # Combine the result with the original image 245 | result = cv2.addWeighted(undist, 1, newwarp, 0.3, 0) 246 | 247 | return result 248 | 249 | def detect_lane(self, img): 250 | img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR) 251 | undist = cv2.undistort(img, self.mtx, self.dist, None, self.mtx) 252 | color_binary, combined_binary = self.thresholding_pipeline(undist, self.s_thresh, self.sx_thresh) 253 | binary_warped = cv2.warpPerspective(combined_binary, self.perspective_M ,self.img_size, flags=cv2.INTER_LINEAR) 254 | self.get_lane_features(binary_warped) 255 | out = self.draw_lane_path(binary_warped, undist) 256 | 257 | return out -------------------------------------------------------------------------------- /examples/line.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | class Line(): 4 | def __init__(self): 5 | # was the line detected in the last iteration? 6 | self.detected = False 7 | # x values of the last n fits of the line 8 | self.recent_xfitted = [] 9 | #average x values of the fitted line over the last n iterations 10 | self.bestx = None 11 | #polynomial coefficients averaged over the last n iterations 12 | self.best_fit = None 13 | #polynomial coefficients for last n fits 14 | self.current_fit = [] 15 | #radius of curvature of the line in some units 16 | self.radius_of_curvature = None 17 | #distance in meters of vehicle center from the line 18 | self.line_base_pos = None 19 | #difference in fit coefficients between last and new fits 20 | self.diffs = np.array([0,0,0], dtype='float') 21 | #x values for detected line pixels 22 | self.allx = None 23 | #y values for detected line pixels 24 | self.ally = None -------------------------------------------------------------------------------- /examples/map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flux-project/flux/0e48aaff31b0ee626e3a2ae507af953658dbcd85/examples/map.png -------------------------------------------------------------------------------- /examples/object_detection_model.py: -------------------------------------------------------------------------------- 1 | from keras.models import Sequential, Model 2 | from keras.layers import Reshape, Activation, Conv2D, Input, MaxPooling2D, BatchNormalization, Flatten, Dense, Lambda 3 | from keras.layers.advanced_activations import LeakyReLU 4 | from keras.callbacks import EarlyStopping, ModelCheckpoint, TensorBoard 5 | from keras.optimizers import SGD, Adam, RMSprop 6 | from keras.layers.merge import concatenate 7 | import numpy as np 8 | import tensorflow as tf 9 | 10 | class YOLO2MODEL: 11 | def __init__(self): 12 | self.LABELS = ['person', 'bicycle', 'car', 'motorcycle', 'airplane', 'bus', 'train', 'truck', 'boat', 'traffic light', 'fire hydrant', 'stop sign', 'parking meter', 'bench', 'bird', 'cat', 'dog', 'horse', 'sheep', 'cow', 'elephant', 'bear', 'zebra', 'giraffe', 'backpack', 'umbrella', 'handbag', 'tie', 'suitcase', 'frisbee', 'skis', 'snowboard', 'sports ball', 'kite', 'baseball bat', 'baseball glove', 'skateboard', 'surfboard', 'tennis racket', 'bottle', 'wine glass', 'cup', 'fork', 'knife', 'spoon', 'bowl', 'banana', 'apple', 'sandwich', 'orange', 'broccoli', 'carrot', 'hot dog', 'pizza', 'donut', 'cake', 'chair', 'couch', 'potted plant', 'bed', 'dining table', 'toilet', 'tv', 'laptop', 'mouse', 'remote', 'keyboard', 'cell phone', 'microwave', 'oven', 'toaster', 'sink', 'refrigerator', 'book', 'clock', 'vase', 'scissors', 'teddy bear', 'hair drier', 'toothbrush'] 13 | 14 | self.IMAGE_H, self.IMAGE_W = 416, 416 15 | self.GRID_H, self.GRID_W = 13 , 13 16 | self.BOX = 5 17 | self.CLASSES = len(self.LABELS) 18 | self.CLASS_WEIGHTS = np.ones(self.CLASSES, dtype='float32') 19 | self.OBJ_THRESHOLD = 0.3 20 | self.NMS_THRESHOLD = 0.3 21 | self.ANCHORS = [0.57273, 0.677385, 1.87446, 2.06253, 3.33843, 5.47434, 7.88282, 3.52778, 9.77052, 9.16828] 22 | 23 | self.NO_OBJECT_SCALE = 1.0 24 | self.OBJECT_SCALE = 5.0 25 | self.COORD_SCALE = 1.0 26 | self.CLASS_SCALE = 1.0 27 | 28 | self.BATCH_SIZE = 16 29 | self.WARM_UP_BATCHES = 0 30 | self.TRUE_BOX_BUFFER = 50 31 | 32 | def space_to_depth_x2(self,x): 33 | return tf.space_to_depth(x, block_size=2) 34 | 35 | def build(self): 36 | input_image = Input(shape=(self.IMAGE_H, self.IMAGE_W, 3)) 37 | true_boxes = Input(shape=(1, 1, 1, self.TRUE_BOX_BUFFER , 4)) 38 | # Layer 1 39 | x = Conv2D(32, (3,3), strides=(1,1), padding='same', name='conv_1', use_bias=False)(input_image) 40 | x = BatchNormalization(name='norm_1')(x) 41 | x = LeakyReLU(alpha=0.1)(x) 42 | x = MaxPooling2D(pool_size=(2, 2))(x) 43 | 44 | # Layer 2 45 | x = Conv2D(64, (3,3), strides=(1,1), padding='same', name='conv_2', use_bias=False)(x) 46 | x = BatchNormalization(name='norm_2')(x) 47 | x = LeakyReLU(alpha=0.1)(x) 48 | x = MaxPooling2D(pool_size=(2, 2))(x) 49 | 50 | # Layer 3 51 | x = Conv2D(128, (3,3), strides=(1,1), padding='same', name='conv_3', use_bias=False)(x) 52 | x = BatchNormalization(name='norm_3')(x) 53 | x = LeakyReLU(alpha=0.1)(x) 54 | 55 | # Layer 4 56 | x = Conv2D(64, (1,1), strides=(1,1), padding='same', name='conv_4', use_bias=False)(x) 57 | x = BatchNormalization(name='norm_4')(x) 58 | x = LeakyReLU(alpha=0.1)(x) 59 | 60 | # Layer 5 61 | x = Conv2D(128, (3,3), strides=(1,1), padding='same', name='conv_5', use_bias=False)(x) 62 | x = BatchNormalization(name='norm_5')(x) 63 | x = LeakyReLU(alpha=0.1)(x) 64 | x = MaxPooling2D(pool_size=(2, 2))(x) 65 | 66 | # Layer 6 67 | x = Conv2D(256, (3,3), strides=(1,1), padding='same', name='conv_6', use_bias=False)(x) 68 | x = BatchNormalization(name='norm_6')(x) 69 | x = LeakyReLU(alpha=0.1)(x) 70 | 71 | # Layer 7 72 | x = Conv2D(128, (1,1), strides=(1,1), padding='same', name='conv_7', use_bias=False)(x) 73 | x = BatchNormalization(name='norm_7')(x) 74 | x = LeakyReLU(alpha=0.1)(x) 75 | 76 | # Layer 8 77 | x = Conv2D(256, (3,3), strides=(1,1), padding='same', name='conv_8', use_bias=False)(x) 78 | x = BatchNormalization(name='norm_8')(x) 79 | x = LeakyReLU(alpha=0.1)(x) 80 | x = MaxPooling2D(pool_size=(2, 2))(x) 81 | 82 | # Layer 9 83 | x = Conv2D(512, (3,3), strides=(1,1), padding='same', name='conv_9', use_bias=False)(x) 84 | x = BatchNormalization(name='norm_9')(x) 85 | x = LeakyReLU(alpha=0.1)(x) 86 | 87 | # Layer 10 88 | x = Conv2D(256, (1,1), strides=(1,1), padding='same', name='conv_10', use_bias=False)(x) 89 | x = BatchNormalization(name='norm_10')(x) 90 | x = LeakyReLU(alpha=0.1)(x) 91 | 92 | # Layer 11 93 | x = Conv2D(512, (3,3), strides=(1,1), padding='same', name='conv_11', use_bias=False)(x) 94 | x = BatchNormalization(name='norm_11')(x) 95 | x = LeakyReLU(alpha=0.1)(x) 96 | 97 | # Layer 12 98 | x = Conv2D(256, (1,1), strides=(1,1), padding='same', name='conv_12', use_bias=False)(x) 99 | x = BatchNormalization(name='norm_12')(x) 100 | x = LeakyReLU(alpha=0.1)(x) 101 | 102 | # Layer 13 103 | x = Conv2D(512, (3,3), strides=(1,1), padding='same', name='conv_13', use_bias=False)(x) 104 | x = BatchNormalization(name='norm_13')(x) 105 | x = LeakyReLU(alpha=0.1)(x) 106 | 107 | skip_connection = x 108 | 109 | x = MaxPooling2D(pool_size=(2, 2))(x) 110 | 111 | # Layer 14 112 | x = Conv2D(1024, (3,3), strides=(1,1), padding='same', name='conv_14', use_bias=False)(x) 113 | x = BatchNormalization(name='norm_14')(x) 114 | x = LeakyReLU(alpha=0.1)(x) 115 | 116 | # Layer 15 117 | x = Conv2D(512, (1,1), strides=(1,1), padding='same', name='conv_15', use_bias=False)(x) 118 | x = BatchNormalization(name='norm_15')(x) 119 | x = LeakyReLU(alpha=0.1)(x) 120 | 121 | # Layer 16 122 | x = Conv2D(1024, (3,3), strides=(1,1), padding='same', name='conv_16', use_bias=False)(x) 123 | x = BatchNormalization(name='norm_16')(x) 124 | x = LeakyReLU(alpha=0.1)(x) 125 | 126 | # Layer 17 127 | x = Conv2D(512, (1,1), strides=(1,1), padding='same', name='conv_17', use_bias=False)(x) 128 | x = BatchNormalization(name='norm_17')(x) 129 | x = LeakyReLU(alpha=0.1)(x) 130 | 131 | # Layer 18 132 | x = Conv2D(1024, (3,3), strides=(1,1), padding='same', name='conv_18', use_bias=False)(x) 133 | x = BatchNormalization(name='norm_18')(x) 134 | x = LeakyReLU(alpha=0.1)(x) 135 | 136 | # Layer 19 137 | x = Conv2D(1024, (3,3), strides=(1,1), padding='same', name='conv_19', use_bias=False)(x) 138 | x = BatchNormalization(name='norm_19')(x) 139 | x = LeakyReLU(alpha=0.1)(x) 140 | 141 | # Layer 20 142 | x = Conv2D(1024, (3,3), strides=(1,1), padding='same', name='conv_20', use_bias=False)(x) 143 | x = BatchNormalization(name='norm_20')(x) 144 | x = LeakyReLU(alpha=0.1)(x) 145 | 146 | # Layer 21 147 | skip_connection = Conv2D(64, (1,1), strides=(1,1), padding='same', name='conv_21', use_bias=False)(skip_connection) 148 | skip_connection = BatchNormalization(name='norm_21')(skip_connection) 149 | skip_connection = LeakyReLU(alpha=0.1)(skip_connection) 150 | skip_connection = Lambda(self.space_to_depth_x2)(skip_connection) 151 | 152 | x = concatenate([skip_connection, x]) 153 | 154 | # Layer 22 155 | x = Conv2D(1024, (3,3), strides=(1,1), padding='same', name='conv_22', use_bias=False)(x) 156 | x = BatchNormalization(name='norm_22')(x) 157 | x = LeakyReLU(alpha=0.1)(x) 158 | 159 | # Layer 23 160 | x = Conv2D(self.BOX * (4 + 1 + self.CLASSES), (1,1), strides=(1,1), padding='same', name='conv_23')(x) 161 | output = Reshape((self.GRID_H, self.GRID_W, self.BOX, 4 + 1 + self.CLASSES))(x) 162 | 163 | # small hack to allow true_boxes to be registered when Keras build the model 164 | # for more information: https://github.com/fchollet/keras/issues/2790 165 | output = Lambda(lambda args: args[0])([output, true_boxes]) 166 | 167 | model = Model([input_image, true_boxes], output) 168 | 169 | return model -------------------------------------------------------------------------------- /examples/object_detector.py: -------------------------------------------------------------------------------- 1 | from keras.models import Sequential, Model 2 | from keras.layers import Reshape, Activation, Conv2D, Input, MaxPooling2D, BatchNormalization, Flatten, Dense, Lambda 3 | from keras.layers.advanced_activations import LeakyReLU 4 | from keras.callbacks import EarlyStopping, ModelCheckpoint, TensorBoard 5 | from keras.optimizers import SGD, Adam, RMSprop 6 | from keras.layers.merge import concatenate 7 | import matplotlib.pyplot as plt 8 | import keras.backend as K 9 | import tensorflow as tf 10 | from tqdm import tqdm 11 | import numpy as np 12 | import pickle 13 | import os, cv2 14 | from utils import WeightReader, decode_netout, draw_boxes 15 | from object_detection_model import YOLO2MODEL 16 | 17 | class ObjectDetector: 18 | def __init__(self, weights_path): 19 | self.wt_path = weights_path 20 | self.yoloModelObj = YOLO2MODEL() 21 | self.model = self.yoloModelObj.build() 22 | self.load_model_weights() 23 | 24 | def load_model_weights(self): 25 | weight_reader = WeightReader(self.wt_path) 26 | #weight_reader.reset() 27 | nb_conv = 23 28 | 29 | for i in range(1, nb_conv+1): 30 | conv_layer = self.model.get_layer('conv_' + str(i)) 31 | 32 | if i < nb_conv: 33 | norm_layer = self.model.get_layer('norm_' + str(i)) 34 | 35 | size = np.prod(norm_layer.get_weights()[0].shape) 36 | 37 | beta = weight_reader.read_bytes(size) 38 | gamma = weight_reader.read_bytes(size) 39 | mean = weight_reader.read_bytes(size) 40 | var = weight_reader.read_bytes(size) 41 | 42 | weights = norm_layer.set_weights([gamma, beta, mean, var]) 43 | 44 | if len(conv_layer.get_weights()) > 1: 45 | bias = weight_reader.read_bytes(np.prod(conv_layer.get_weights()[1].shape)) 46 | kernel = weight_reader.read_bytes(np.prod(conv_layer.get_weights()[0].shape)) 47 | kernel = kernel.reshape(list(reversed(conv_layer.get_weights()[0].shape))) 48 | kernel = kernel.transpose([2,3,1,0]) 49 | conv_layer.set_weights([kernel, bias]) 50 | else: 51 | kernel = weight_reader.read_bytes(np.prod(conv_layer.get_weights()[0].shape)) 52 | kernel = kernel.reshape(list(reversed(conv_layer.get_weights()[0].shape))) 53 | kernel = kernel.transpose([2,3,1,0]) 54 | conv_layer.set_weights([kernel]) 55 | 56 | return 57 | 58 | def detect_obj(self, image): 59 | dummy_array = np.zeros((1,1,1,1,self.yoloModelObj.TRUE_BOX_BUFFER,4)) 60 | input_image = cv2.resize(image, (416, 416)) 61 | input_image = input_image / 255. 62 | input_image = input_image[:,:,::-1] 63 | input_image = np.expand_dims(input_image, 0) 64 | 65 | netout = self.model.predict([input_image, dummy_array]) 66 | 67 | boxes = decode_netout(netout[0], 68 | obj_threshold=self.yoloModelObj.OBJ_THRESHOLD, 69 | nms_threshold=self.yoloModelObj.NMS_THRESHOLD, 70 | anchors=self.yoloModelObj.ANCHORS, 71 | nb_class=self.yoloModelObj.CLASSES) 72 | 73 | image = draw_boxes(image, boxes, labels=self.yoloModelObj.LABELS) 74 | 75 | return image 76 | -------------------------------------------------------------------------------- /examples/rosbag-larger-than-2-GB.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Let us have a look at a 20 GB Rosbag file\n", 8 | "**Note** data can be found for instance at https://github.com/udacity/self-driving-car/tree/master/datasets published under MIT License.\n", 9 | "\n", 10 | "The file is not distributed over the Dockerfile but you can download it and put it into HDFS." 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": 1, 16 | "metadata": {}, 17 | "outputs": [ 18 | { 19 | "name": "stdout", 20 | "output_type": "stream", 21 | "text": [ 22 | "-rw-r--r-- 1 root root 20G Mar 7 15:16 /root/project/doc/el_camino_north.bag\n" 23 | ] 24 | } 25 | ], 26 | "source": [ 27 | "%%bash\n", 28 | "\n", 29 | "ls -tralFh /root/project/doc/el_camino_north.bag" 30 | ] 31 | }, 32 | { 33 | "cell_type": "code", 34 | "execution_count": 2, 35 | "metadata": {}, 36 | "outputs": [ 37 | { 38 | "name": "stdout", 39 | "output_type": "stream", 40 | "text": [ 41 | "Found 2 items\n", 42 | "-rw-r--r-- 1 root supergroup 331.6 M 2018-03-06 20:50 HMB_4.bag\n", 43 | "-rw-r--r-- 1 root supergroup 19.7 G 2018-03-07 15:28 el_camino_north.bag\n" 44 | ] 45 | } 46 | ], 47 | "source": [ 48 | "%%bash\n", 49 | "\n", 50 | "# same size, no worries, just the -h (human) formating differs in rounding \n", 51 | "hdfs dfs -ls -h" 52 | ] 53 | }, 54 | { 55 | "cell_type": "markdown", 56 | "metadata": {}, 57 | "source": [ 58 | "# Show that the we can read the index\n", 59 | "\n", 60 | "Solved the issue https://github.com/valtech/ros_hadoop/issues/6 \n", 61 | "\n", 62 | "The issue was due to ByteBuffer being limitted by JVM Integer size and has nothing to do with Spark or how the RosbagMapInputFormat works within Spark. It was only problematic to extract the conf index with the jar.\n", 63 | "\n", 64 | "Integer.MAX_SIZE is 2 GB !!" 65 | ] 66 | }, 67 | { 68 | "cell_type": "code", 69 | "execution_count": 3, 70 | "metadata": {}, 71 | "outputs": [ 72 | { 73 | "name": "stdout", 74 | "output_type": "stream", 75 | "text": [ 76 | "CPU times: user 10 ms, sys: 0 ns, total: 10 ms\n", 77 | "Wall time: 1.18 s\n" 78 | ] 79 | } 80 | ], 81 | "source": [ 82 | "%%time\n", 83 | "\n", 84 | "out = !java -jar ../lib/rosbaginputformat.jar -f /root/project/doc/el_camino_north.bag" 85 | ] 86 | }, 87 | { 88 | "cell_type": "code", 89 | "execution_count": 4, 90 | "metadata": {}, 91 | "outputs": [ 92 | { 93 | "name": "stdout", 94 | "output_type": "stream", 95 | "text": [ 96 | "-rw-r--r-- 1 root root 20G Mar 7 15:16 /root/project/doc/el_camino_north.bag\n", 97 | "-rw-r--r-- 1 root root 62K Mar 7 15:41 /root/project/doc/el_camino_north.bag.idx.bin\n" 98 | ] 99 | } 100 | ], 101 | "source": [ 102 | "%%bash\n", 103 | "ls -tralFh /root/project/doc/el_camino_north.bag*" 104 | ] 105 | }, 106 | { 107 | "cell_type": "markdown", 108 | "metadata": {}, 109 | "source": [ 110 | "# Create the Spark Session or get an existing one" 111 | ] 112 | }, 113 | { 114 | "cell_type": "code", 115 | "execution_count": 5, 116 | "metadata": {}, 117 | "outputs": [], 118 | "source": [ 119 | "from pyspark import SparkContext, SparkConf\n", 120 | "from pyspark.sql import SparkSession\n", 121 | "\n", 122 | "sparkConf = SparkConf()\n", 123 | "sparkConf.setMaster(\"local[*]\")\n", 124 | "sparkConf.setAppName(\"ros_hadoop\")\n", 125 | "sparkConf.set(\"spark.jars\", \"../lib/protobuf-java-3.3.0.jar,../lib/rosbaginputformat.jar,../lib/scala-library-2.11.8.jar\")\n", 126 | "\n", 127 | "spark = SparkSession.builder.config(conf=sparkConf).getOrCreate()\n", 128 | "sc = spark.sparkContext" 129 | ] 130 | }, 131 | { 132 | "cell_type": "markdown", 133 | "metadata": {}, 134 | "source": [ 135 | "## Create an RDD from the Rosbag file\n", 136 | "**Note:** your HDFS address might differ." 137 | ] 138 | }, 139 | { 140 | "cell_type": "code", 141 | "execution_count": 6, 142 | "metadata": {}, 143 | "outputs": [], 144 | "source": [ 145 | "fin = sc.newAPIHadoopFile(\n", 146 | " path = \"hdfs://127.0.0.1:9000/user/root/el_camino_north.bag\",\n", 147 | " inputFormatClass = \"de.valtech.foss.RosbagMapInputFormat\",\n", 148 | " keyClass = \"org.apache.hadoop.io.LongWritable\",\n", 149 | " valueClass = \"org.apache.hadoop.io.MapWritable\",\n", 150 | " conf = {\"RosbagInputFormat.chunkIdx\":\"/root/project/doc/el_camino_north.bag.idx.bin\"})" 151 | ] 152 | }, 153 | { 154 | "cell_type": "code", 155 | "execution_count": 14, 156 | "metadata": {}, 157 | "outputs": [ 158 | { 159 | "data": { 160 | "text/plain": [ 161 | "MapPartitionsRDD[2] at mapPartitions at SerDeUtil.scala:244" 162 | ] 163 | }, 164 | "execution_count": 14, 165 | "metadata": {}, 166 | "output_type": "execute_result" 167 | } 168 | ], 169 | "source": [ 170 | "fin" 171 | ] 172 | }, 173 | { 174 | "attachments": { 175 | "image.png": { 176 | "image/png": "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" 177 | } 178 | }, 179 | "cell_type": "markdown", 180 | "metadata": {}, 181 | "source": [ 182 | "### On a laptop it will take some time. So please run it on a real cluster.\n", 183 | "\n", 184 | "![image.png](attachment:image.png)\n", 185 | "\n", 186 | "# Have Fun!" 187 | ] 188 | } 189 | ], 190 | "metadata": { 191 | "kernelspec": { 192 | "display_name": "Python 2", 193 | "language": "python", 194 | "name": "python2" 195 | }, 196 | "language_info": { 197 | "codemirror_mode": { 198 | "name": "ipython", 199 | "version": 2 200 | }, 201 | "file_extension": ".py", 202 | "mimetype": "text/x-python", 203 | "name": "python", 204 | "nbconvert_exporter": "python", 205 | "pygments_lexer": "ipython2", 206 | "version": "2.7.12" 207 | } 208 | }, 209 | "nbformat": 4, 210 | "nbformat_minor": 2 211 | } 212 | -------------------------------------------------------------------------------- /examples/utils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import os 3 | import xml.etree.ElementTree as ET 4 | import tensorflow as tf 5 | import copy 6 | import cv2 7 | 8 | class BoundBox: 9 | def __init__(self, xmin, ymin, xmax, ymax, c = None, classes = None): 10 | self.xmin = xmin 11 | self.ymin = ymin 12 | self.xmax = xmax 13 | self.ymax = ymax 14 | 15 | self.c = c 16 | self.classes = classes 17 | 18 | self.label = -1 19 | self.score = -1 20 | 21 | def get_label(self): 22 | if self.label == -1: 23 | self.label = np.argmax(self.classes) 24 | 25 | return self.label 26 | 27 | def get_score(self): 28 | if self.score == -1: 29 | self.score = self.classes[self.get_label()] 30 | 31 | return self.score 32 | 33 | class WeightReader: 34 | def __init__(self, weight_file): 35 | self.offset = 4 36 | self.all_weights = np.fromfile(weight_file, dtype='float32') 37 | 38 | def read_bytes(self, size): 39 | self.offset = self.offset + size 40 | return self.all_weights[self.offset-size:self.offset] 41 | 42 | def reset(self): 43 | self.offset = 4 44 | 45 | def bbox_iou(box1, box2): 46 | intersect_w = _interval_overlap([box1.xmin, box1.xmax], [box2.xmin, box2.xmax]) 47 | intersect_h = _interval_overlap([box1.ymin, box1.ymax], [box2.ymin, box2.ymax]) 48 | 49 | intersect = intersect_w * intersect_h 50 | 51 | w1, h1 = box1.xmax-box1.xmin, box1.ymax-box1.ymin 52 | w2, h2 = box2.xmax-box2.xmin, box2.ymax-box2.ymin 53 | 54 | union = w1*h1 + w2*h2 - intersect 55 | 56 | return float(intersect) / union 57 | 58 | def draw_boxes(image, boxes, labels): 59 | image_h, image_w, _ = image.shape 60 | 61 | for box in boxes: 62 | xmin = int(box.xmin*image_w) 63 | ymin = int(box.ymin*image_h) 64 | xmax = int(box.xmax*image_w) 65 | ymax = int(box.ymax*image_h) 66 | 67 | cv2.rectangle(image, (xmin,ymin), (xmax,ymax), (0,255,0), 3) 68 | cv2.putText(image, 69 | labels[box.get_label()] + ' ' + str(box.get_score()), 70 | (xmin, ymin - 13), 71 | cv2.FONT_HERSHEY_SIMPLEX, 72 | 1e-3 * image_h, 73 | (0,255,0), 2) 74 | 75 | return image 76 | 77 | def decode_netout(netout, anchors, nb_class, obj_threshold=0.3, nms_threshold=0.3): 78 | grid_h, grid_w, nb_box = netout.shape[:3] 79 | 80 | boxes = [] 81 | 82 | # decode the output by the network 83 | netout[..., 4] = _sigmoid(netout[..., 4]) 84 | netout[..., 5:] = netout[..., 4][..., np.newaxis] * _softmax(netout[..., 5:]) 85 | netout[..., 5:] *= netout[..., 5:] > obj_threshold 86 | 87 | for row in range(grid_h): 88 | for col in range(grid_w): 89 | for b in range(nb_box): 90 | # from 4th element onwards are confidence and class classes 91 | classes = netout[row,col,b,5:] 92 | 93 | if np.sum(classes) > 0: 94 | # first 4 elements are x, y, w, and h 95 | x, y, w, h = netout[row,col,b,:4] 96 | 97 | x = (col + _sigmoid(x)) / grid_w # center position, unit: image width 98 | y = (row + _sigmoid(y)) / grid_h # center position, unit: image height 99 | w = anchors[2 * b + 0] * np.exp(w) / grid_w # unit: image width 100 | h = anchors[2 * b + 1] * np.exp(h) / grid_h # unit: image height 101 | confidence = netout[row,col,b,4] 102 | 103 | box = BoundBox(x-w/2, y-h/2, x+w/2, y+h/2, confidence, classes) 104 | 105 | boxes.append(box) 106 | 107 | # suppress non-maximal boxes 108 | for c in range(nb_class): 109 | sorted_indices = list(reversed(np.argsort([box.classes[c] for box in boxes]))) 110 | 111 | for i in range(len(sorted_indices)): 112 | index_i = sorted_indices[i] 113 | 114 | if boxes[index_i].classes[c] == 0: 115 | continue 116 | else: 117 | for j in range(i+1, len(sorted_indices)): 118 | index_j = sorted_indices[j] 119 | 120 | if bbox_iou(boxes[index_i], boxes[index_j]) >= nms_threshold: 121 | boxes[index_j].classes[c] = 0 122 | 123 | # remove the boxes which are less likely than a obj_threshold 124 | boxes = [box for box in boxes if box.get_score() > obj_threshold] 125 | 126 | return boxes 127 | 128 | def compute_overlap(a, b): 129 | """ 130 | Code originally from https://github.com/rbgirshick/py-faster-rcnn. 131 | Parameters 132 | ---------- 133 | a: (N, 4) ndarray of float 134 | b: (K, 4) ndarray of float 135 | Returns 136 | ------- 137 | overlaps: (N, K) ndarray of overlap between boxes and query_boxes 138 | """ 139 | area = (b[:, 2] - b[:, 0]) * (b[:, 3] - b[:, 1]) 140 | 141 | iw = np.minimum(np.expand_dims(a[:, 2], axis=1), b[:, 2]) - np.maximum(np.expand_dims(a[:, 0], 1), b[:, 0]) 142 | ih = np.minimum(np.expand_dims(a[:, 3], axis=1), b[:, 3]) - np.maximum(np.expand_dims(a[:, 1], 1), b[:, 1]) 143 | 144 | iw = np.maximum(iw, 0) 145 | ih = np.maximum(ih, 0) 146 | 147 | ua = np.expand_dims((a[:, 2] - a[:, 0]) * (a[:, 3] - a[:, 1]), axis=1) + area - iw * ih 148 | 149 | ua = np.maximum(ua, np.finfo(float).eps) 150 | 151 | intersection = iw * ih 152 | 153 | return intersection / ua 154 | 155 | def compute_ap(recall, precision): 156 | """ Compute the average precision, given the recall and precision curves. 157 | Code originally from https://github.com/rbgirshick/py-faster-rcnn. 158 | 159 | # Arguments 160 | recall: The recall curve (list). 161 | precision: The precision curve (list). 162 | # Returns 163 | The average precision as computed in py-faster-rcnn. 164 | """ 165 | # correct AP calculation 166 | # first append sentinel values at the end 167 | mrec = np.concatenate(([0.], recall, [1.])) 168 | mpre = np.concatenate(([0.], precision, [0.])) 169 | 170 | # compute the precision envelope 171 | for i in range(mpre.size - 1, 0, -1): 172 | mpre[i - 1] = np.maximum(mpre[i - 1], mpre[i]) 173 | 174 | # to calculate area under PR curve, look for points 175 | # where X axis (recall) changes value 176 | i = np.where(mrec[1:] != mrec[:-1])[0] 177 | 178 | # and sum (\Delta recall) * prec 179 | ap = np.sum((mrec[i + 1] - mrec[i]) * mpre[i + 1]) 180 | return ap 181 | 182 | def _interval_overlap(interval_a, interval_b): 183 | x1, x2 = interval_a 184 | x3, x4 = interval_b 185 | 186 | if x3 < x1: 187 | if x4 < x1: 188 | return 0 189 | else: 190 | return min(x2,x4) - x1 191 | else: 192 | if x2 < x3: 193 | return 0 194 | else: 195 | return min(x2,x4) - x3 196 | 197 | def _sigmoid(x): 198 | return 1. / (1. + np.exp(-x)) 199 | 200 | def _softmax(x, axis=-1, t=-100.): 201 | x = x - np.max(x) 202 | 203 | if np.min(x) < t: 204 | x = x/np.min(x)*t 205 | 206 | e_x = np.exp(x) 207 | 208 | return e_x / e_x.sum(axis, keepdims=True) -------------------------------------------------------------------------------- /images/flux_cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flux-project/flux/0e48aaff31b0ee626e3a2ae507af953658dbcd85/images/flux_cloud.png -------------------------------------------------------------------------------- /images/flux_overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flux-project/flux/0e48aaff31b0ee626e3a2ae507af953658dbcd85/images/flux_overview.png -------------------------------------------------------------------------------- /images/login_notebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flux-project/flux/0e48aaff31b0ee626e3a2ae507af953658dbcd85/images/login_notebook.png -------------------------------------------------------------------------------- /images/sample_notebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flux-project/flux/0e48aaff31b0ee626e3a2ae507af953658dbcd85/images/sample_notebook.png --------------------------------------------------------------------------------