├── demo-suite ├── demos │ ├── __init__.py │ ├── fractal │ │ ├── __init__.py │ │ ├── app.yaml │ │ ├── TODO.md │ │ ├── createpds.sh │ │ ├── static │ │ │ ├── css │ │ │ │ └── style.css │ │ │ └── js │ │ │ │ ├── throttled_image_map.js │ │ │ │ └── script.js │ │ ├── vm_files │ │ │ ├── startup.sh │ │ │ ├── map_test.html │ │ │ └── mandelbrot.go │ │ ├── templates │ │ │ └── index.html │ │ └── main.py │ ├── image-magick │ │ ├── __init__.py │ │ ├── app.yaml │ │ ├── static │ │ │ ├── css │ │ │ │ └── style.css │ │ │ └── js │ │ │ │ └── script.js │ │ ├── startup.sh │ │ ├── templates │ │ │ └── index.html │ │ └── main.py │ └── quick-start │ │ ├── __init__.py │ │ ├── app.yaml │ │ ├── static │ │ ├── css │ │ │ └── style.css │ │ └── js │ │ │ └── script.js │ │ ├── templates │ │ └── index.html │ │ └── main.py ├── lib │ ├── __init__.py │ ├── google_cloud │ │ ├── __init__.py │ │ ├── oauth.py │ │ ├── gce_exception.py │ │ ├── gcs_appengine.py │ │ ├── cs.py │ │ └── gce_appengine.py │ └── user_data.py ├── .gitignore ├── static │ ├── bootstrap │ │ ├── img │ │ │ ├── glyphicons-halflings.png │ │ │ └── glyphicons-halflings-white.png │ │ └── js │ │ │ └── bootstrap-button.js │ ├── fontawesome │ │ ├── font │ │ │ ├── fontawesome-webfont.eot │ │ │ ├── fontawesome-webfont.ttf │ │ │ └── fontawesome-webfont.woff │ │ ├── README.md │ │ └── css │ │ │ └── font-awesome.min.css │ ├── css │ │ └── style.css │ └── js │ │ ├── util.js │ │ ├── timer.js │ │ ├── squares.js │ │ ├── counter.js │ │ └── gce.js ├── discovery │ └── compute │ │ └── README ├── settings.json ├── templates │ ├── index.html │ ├── project.html │ └── base.html ├── app.yaml ├── main.py └── lib_path.py ├── .gitignore ├── TODOS ├── download_dependencies.sh ├── DESIGN.md ├── CONTRIB.md ├── README.md └── LICENSE /demo-suite/demos/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo-suite/lib/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo-suite/.gitignore: -------------------------------------------------------------------------------- 1 | ext_lib/ 2 | -------------------------------------------------------------------------------- /demo-suite/demos/fractal/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo-suite/demos/image-magick/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo-suite/demos/quick-start/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo-suite/lib/google_cloud/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo-suite/static/bootstrap/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlearchive/compute-appengine-demo-suite-python/HEAD/demo-suite/static/bootstrap/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /demo-suite/static/fontawesome/font/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlearchive/compute-appengine-demo-suite-python/HEAD/demo-suite/static/fontawesome/font/fontawesome-webfont.eot -------------------------------------------------------------------------------- /demo-suite/static/fontawesome/font/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlearchive/compute-appengine-demo-suite-python/HEAD/demo-suite/static/fontawesome/font/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /demo-suite/static/fontawesome/font/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlearchive/compute-appengine-demo-suite-python/HEAD/demo-suite/static/fontawesome/font/fontawesome-webfont.woff -------------------------------------------------------------------------------- /demo-suite/static/bootstrap/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlearchive/compute-appengine-demo-suite-python/HEAD/demo-suite/static/bootstrap/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /demo-suite/demos/fractal/app.yaml: -------------------------------------------------------------------------------- 1 | handlers: 2 | - url: /fractal/css 3 | static_dir: demos/fractal/static/css 4 | 5 | - url: /fractal/js 6 | static_dir: demos/fractal/static/js 7 | 8 | - url: /fractal.* 9 | script: demos.fractal.main.app 10 | -------------------------------------------------------------------------------- /demo-suite/demos/quick-start/app.yaml: -------------------------------------------------------------------------------- 1 | handlers: 2 | - url: /quick-start/css 3 | static_dir: demos/quick-start/static/css 4 | 5 | - url: /quick-start/js 6 | static_dir: demos/quick-start/static/js 7 | 8 | - url: /quick-start.* 9 | script: demos.quick-start.main.app 10 | -------------------------------------------------------------------------------- /demo-suite/demos/image-magick/app.yaml: -------------------------------------------------------------------------------- 1 | handlers: 2 | - url: /image-magick/css 3 | static_dir: demos/image-magick/static/css 4 | 5 | - url: /image-magick/js 6 | static_dir: demos/image-magick/static/js 7 | 8 | - url: /image-magick.* 9 | script: demos.image-magick.main.app 10 | -------------------------------------------------------------------------------- /demo-suite/demos/image-magick/static/css/style.css: -------------------------------------------------------------------------------- 1 | h3 { 2 | margin-top: 20px; 3 | } 4 | 5 | img { 6 | height: 30px; 7 | width: 30px; 8 | } 9 | 10 | label { 11 | width: 350px; 12 | display: block; 13 | float: left; 14 | } 15 | 16 | p { 17 | clear: left; 18 | } 19 | 20 | #instances { 21 | margin-bottom: 20px; 22 | } 23 | 24 | .project { 25 | width: 150px; 26 | } 27 | -------------------------------------------------------------------------------- /demo-suite/discovery/compute/README: -------------------------------------------------------------------------------- 1 | This directory is for storing local copies of discovery documents, which 2 | speeds up performance. The small gain in performance does not seem like 3 | it would compensate for the problem of a static API, which cannot adapt 4 | to changes, so this feature has been disabled in lib/google_cloud/gce.py, 5 | however, this directory (and it's previous contents) remain intact in case 6 | we decide to reuse local discovery docs in the future. 7 | -------------------------------------------------------------------------------- /demo-suite/demos/fractal/TODO.md: -------------------------------------------------------------------------------- 1 | * Better map zooming - currently, the left map controls both maps. Both maps 2 | should have the same control. 3 | * Make it easier to update program on VMs without restarting. Push new 4 | program/params and have something in guest quit and be restarted. 5 | * Add a memcached layer for caching tiles 6 | * Pound on the thing with apache bench 7 | * Run the 16 servers across 2 zones. 8 | 9 | * Make errors less visible 10 | * Debug rare Oauth error 11 | * Implement client health check of LB state 12 | -------------------------------------------------------------------------------- /demo-suite/demos/quick-start/static/css/style.css: -------------------------------------------------------------------------------- 1 | h3 { 2 | margin: 20px 0; 3 | } 4 | 5 | img { 6 | height: 30px; 7 | width: 30px; 8 | } 9 | 10 | label { 11 | width: 350px; 12 | display: block; 13 | float: left; 14 | } 15 | 16 | p { 17 | clear: left; 18 | } 19 | 20 | #num-instances { 21 | width: 55px; 22 | margin-top: 12px; 23 | line-height: 18px; 24 | } 25 | 26 | #instances { 27 | margin-bottom: 20px; 28 | } 29 | 30 | #elapsed-time { 31 | font-size: 25px; 32 | } 33 | 34 | .project { 35 | width: 150px; 36 | } 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | ./lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | 32 | # Mr Developer 33 | .mr.developer.cfg 34 | .project 35 | .pydevproject 36 | 37 | # temporary files downloaded when fetching dependencies 38 | temp_download/ 39 | -------------------------------------------------------------------------------- /demo-suite/demos/fractal/createpds.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | set -o xtrace 3 | 4 | PROJECT=your-project 5 | ZONE=us-central1-a 6 | GCUTIL="gcutil --project=$PROJECT" 7 | IMAGE=projects/centos-cloud/global/images/centos-6-v20140318 8 | 9 | # Create an array of PD images from an image. 10 | set +o xtrace 11 | PDS="boot-fractal-single-00" 12 | for i in $(seq -f '%02g' 0 15); do 13 | PDS="$PDS boot-fractal-cluster-$i" 14 | done 15 | set -o xtrace 16 | 17 | echo $PDS 18 | 19 | #Delete any existing PDs 20 | $GCUTIL deletedisk -f --zone=$ZONE $PDS 21 | 22 | $GCUTIL adddisk --zone=$ZONE --source_image=$IMAGE $PDS 23 | -------------------------------------------------------------------------------- /TODOS: -------------------------------------------------------------------------------- 1 | A list of project to-dos: 2 | 3 | - Easier customization of app engine and compute engine project information 4 | to allow for out-of-the-box deployment of the demos. 5 | 6 | - Quick start demo: 7 | 8 | - Make sure the user can't start more instances that what are available. 9 | 10 | - Add a legend. 11 | 12 | - Click on boxes to show information about the instance. 13 | 14 | - Resize squares based on the number of instances. 15 | 16 | - Gray out reset button when clicked. 17 | 18 | - Don't allow TERMINATED instances to add to the counter's count. 19 | 20 | - When the page loads, show the current state of the world. 21 | -------------------------------------------------------------------------------- /demo-suite/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "project": "compute-engine-demo", 3 | "compute": { 4 | "api_version": "v1", 5 | "image": "debian-7-wheezy-v20140318", 6 | "image_project": "debian-cloud", 7 | "machine_type": "n1-standard-1", 8 | "zone": "us-central1-f", 9 | "network": "default", 10 | "access_configs": [], 11 | "firewall": { 12 | "allowed": [{"IPProtocol": "tcp", "ports": "80"}], 13 | "sourceRanges": ["0.0.0.0/0"]} 14 | }, 15 | "cloud_service_account": [{ 16 | "email": "default", 17 | "scopes": ["https://www.googleapis.com/auth/devstorage.full_control"]}] 18 | } 19 | -------------------------------------------------------------------------------- /demo-suite/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "templates/base.html" %} 2 | 3 | {% block content %} 4 |

5 | Welcome to the Compute Engine Demos site! 6 |

7 | 22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /demo-suite/demos/fractal/static/css/style.css: -------------------------------------------------------------------------------- 1 | #spinner { 2 | display:inline-block; 3 | } 4 | 5 | .map-container { 6 | height: 450px; 7 | } 8 | 9 | .squares { 10 | height: 80px; 11 | } 12 | 13 | hr { 14 | margin-top: 3px; 15 | border-top: 2px solid #666; 16 | } 17 | 18 | .stat-container { 19 | background: #666; 20 | color: white; 21 | border-radius: 6px; 22 | border: 1px solid black; 23 | padding: 5px; 24 | } 25 | 26 | .stat-name { 27 | text-align: center; 28 | } 29 | 30 | .stat-value-row { 31 | text-align: center; 32 | } 33 | 34 | .stat-value { 35 | font-size: 18px; 36 | font-weight: bold; 37 | } 38 | 39 | .stat-value:after { 40 | content: ' '; 41 | } 42 | -------------------------------------------------------------------------------- /demo-suite/app.yaml: -------------------------------------------------------------------------------- 1 | application: gce-demos 2 | version: 1 3 | runtime: python27 4 | api_version: 1 5 | threadsafe: true 6 | 7 | includes: 8 | - demos/fractal/app.yaml 9 | - demos/quick-start/app.yaml 10 | - demos/image-magick/app.yaml 11 | 12 | handlers: 13 | - url: /css 14 | static_dir: static/css 15 | 16 | - url: /js 17 | static_dir: static/js 18 | 19 | - url: /bootstrap 20 | static_dir: static/bootstrap 21 | 22 | - url: /fontawesome 23 | static_dir: static/fontawesome 24 | 25 | - url: /oauth2callback.* 26 | script: main.app 27 | 28 | - url: / 29 | script: main.app 30 | 31 | libraries: 32 | - name: jinja2 33 | version: latest 34 | 35 | builtins: 36 | - deferred: on 37 | 38 | inbound_services: 39 | - warmup 40 | -------------------------------------------------------------------------------- /demo-suite/demos/fractal/vm_files/startup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | IMAGE_VERSION=2 3 | IMAGE_MARK=/var/fractal.image.$IMAGE_VERSION 4 | if [ ! -e $IMAGE_MARK ]; 5 | then 6 | pushd /tmp 7 | curl -O https://go.googlecode.com/files/go1.1.linux-amd64.tar.gz 8 | tar -C /usr/local -xzf go1.1.linux-amd64.tar.gz 9 | touch $IMAGE_MARK 10 | popd 11 | fi 12 | 13 | export PATH=$PATH:/usr/local/go/bin 14 | GMV=/usr/share/google/get_metadata_value 15 | 16 | # Restart the server in the background if it fails. 17 | cd /tmp 18 | function runServer { 19 | while : 20 | do 21 | $GMV attributes/goprog > ./program.go 22 | PROG_ARGS=$($GMV attributes/goargs) 23 | CMDLINE="go run ./program.go $PROG_ARGS" 24 | echo "Running $CMDLINE" 25 | $CMDLINE 26 | done 27 | } 28 | runServer & 29 | -------------------------------------------------------------------------------- /demo-suite/templates/project.html: -------------------------------------------------------------------------------- 1 | {% extends "templates/base.html" %} 2 | 3 | {% block extra_css %} 4 | 5 | {% endblock %} 6 | 7 | {% block content %} 8 |

9 | We need some information from you. 10 |

11 | 12 |
13 |
14 | {% for parameter in parameters %} 15 |

16 | 19 | 21 |

22 | {% endfor %} 23 |

24 | 25 |

26 |

27 | * Indicates a required field 28 |

29 |
30 |
31 | {% endblock %} 32 | -------------------------------------------------------------------------------- /download_dependencies.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | set -o xtrace 3 | set -o errexit 4 | 5 | # Clean things out by deleting any current dependencies 6 | rm -rf demo-suite/ext_lib || true 7 | mkdir demo-suite/ext_lib 8 | 9 | # Clean up any failed/aborted previous downloads 10 | rm -rf temp_download || true 11 | mkdir temp_download 12 | cd temp_download 13 | 14 | curl -O https://python-gflags.googlecode.com/files/python-gflags-2.0.tar.gz 15 | tar -C ../demo-suite/ext_lib/ -xzf python-gflags-2.0.tar.gz 16 | 17 | curl -O https://httplib2.googlecode.com/files/httplib2-0.8.tar.gz 18 | tar -C ../demo-suite/ext_lib/ -xzf httplib2-0.8.tar.gz 19 | 20 | curl -O https://google-api-python-client.googlecode.com/files/oauth2client-1.0.tar.gz 21 | tar -C ../demo-suite/ext_lib/ -xzf oauth2client-1.0.tar.gz 22 | 23 | curl -O https://google-api-python-client.googlecode.com/files/google-api-python-client-1.1.tar.gz 24 | tar -C ../demo-suite/ext_lib/ -xzf google-api-python-client-1.1.tar.gz 25 | 26 | # Clean up after ourselves 27 | cd .. 28 | rm -rf temp_download -------------------------------------------------------------------------------- /demo-suite/demos/image-magick/startup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o xtrace 4 | 5 | IMAGE_VERSION=1 6 | IMAGE_MARK=/var/image-magick.image.$IMAGE_VERSION 7 | if [ ! -e $IMAGE_MARK ]; 8 | then 9 | echo Installing Image Magick 10 | apt-get update 11 | apt-get install -y imagemagick 12 | touch $IMAGE_MARK 13 | fi 14 | BASE_URL=http://metadata/0.1/meta-data/attributes 15 | IMAGE=$(curl $BASE_URL/image) 16 | MACHINE_NUM=$(curl $BASE_URL/machine-num) 17 | TAG=$(curl $BASE_URL/tag) 18 | SEQ=$(curl $BASE_URL/seq) 19 | GCS_PATH=$(curl $BASE_URL/gcs-path) 20 | gsutil cp gs://gce-quick-start-demo/input/$IMAGE.png . 21 | echo downloaded image 22 | command='convert -delay 10 $IMAGE.png' 23 | for i in `seq $SEQ`; do 24 | command="$command \\( -clone 0 -distort SRT $i \\)" 25 | done 26 | command="$command -set dispose Background -delete 0 -loop 0 $TAG-$MACHINE_NUM.gif" 27 | eval $command 28 | echo processed image 29 | gsutil cp -a public-read $TAG-$MACHINE_NUM.gif \ 30 | gs://$GCS_PATH/$TAG-$MACHINE_NUM.gif 31 | echo copied to cloud storage 32 | echo finished 33 | -------------------------------------------------------------------------------- /demo-suite/lib/google_cloud/oauth.py: -------------------------------------------------------------------------------- 1 | # Copyright 2012 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Main page for the Google Compute Engine demo suite.""" 16 | 17 | __author__ = 'kbrisbin@google.com (Kathryn Hurley)' 18 | 19 | import os 20 | 21 | import oauth2client.appengine as oauth2client 22 | 23 | decorator = oauth2client.OAuth2DecoratorFromClientSecrets( 24 | os.path.join(os.path.dirname(__file__), 'client_secrets.json'), 25 | scope=['https://www.googleapis.com/auth/compute', 26 | 'https://www.googleapis.com/auth/devstorage.full_control']) 27 | -------------------------------------------------------------------------------- /demo-suite/static/fontawesome/README.md: -------------------------------------------------------------------------------- 1 | #Font Awesome 3.0.2 2 | ##the iconic font designed for use with Twitter Bootstrap 3 | 4 | The full suite of pictographic icons, examples, and documentation can be found at: 5 | http://fortawesome.github.com/Font-Awesome/ 6 | 7 | 8 | ##License 9 | - The Font Awesome font is licensed under the SIL Open Font License - http://scripts.sil.org/OFL 10 | - Font Awesome CSS, LESS, and SASS files are licensed under the MIT License - http://opensource.org/licenses/mit-license.html 11 | - The Font Awesome pictograms are licensed under the CC BY 3.0 License - http://creativecommons.org/licenses/by/3.0/ 12 | - Attribution is no longer required in Font Awesome 3.0, but much appreciated: "Font Awesome by Dave Gandy - http://fortawesome.github.com/Font-Awesome" 13 | 14 | ##Contact 15 | - Email: dave@davegandy.com 16 | - Twitter: http://twitter.com/fortaweso_me 17 | - Work: Lead Product Designer @ http://kyru.us 18 | 19 | ##Changelog 20 | - v3.0.0 - all icons redesigned from scratch, optimized for Bootstrap's 14px default 21 | - v3.0.1 - much improved rendering in webkit, various bugfixes 22 | - v3.0.2 - much improved rendering and alignment in IE7 23 | -------------------------------------------------------------------------------- /demo-suite/lib/google_cloud/gce_exception.py: -------------------------------------------------------------------------------- 1 | # Copyright 2012 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Gce App Engine Exception classes.""" 16 | 17 | __author__ = 'kbrisbin@google.com (Kathryn Hurley)' 18 | 19 | 20 | class GcelibError(Exception): 21 | """Gcelib Error raised when there's an API error.""" 22 | pass 23 | 24 | 25 | class GceError(Exception): 26 | """GceError raised during improper use of the GceAppEngineHelper class.""" 27 | pass 28 | 29 | class GceTokenError(Exception): 30 | """Error raised when there's an issue refreshing the access token.""" 31 | pass 32 | -------------------------------------------------------------------------------- /demo-suite/demos/image-magick/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "templates/base.html" %} 2 | 3 | {% block extra_css %} 4 | 5 | {% endblock %} 6 | 7 | {% block extra_javascript %} 8 | 13 | 14 | 15 | 16 | 17 | {% endblock %} 18 | 19 | {% block content %} 20 |

21 | Welcome to Image Magick! 22 |

23 | 24 |

25 | This demo shows how to store output from instances on Cloud Storage. 26 |

27 | 28 |

29 | Edit your Compute Engine / Cloud 30 | Storage project information. 31 |

32 | 33 |
34 | Start 35 | Reset 36 |
37 |
38 | {% endblock %} 39 | -------------------------------------------------------------------------------- /demo-suite/demos/quick-start/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "templates/base.html" %} 2 | 3 | {% block extra_css %} 4 | 5 | {% endblock %} 6 | 7 | {% block extra_javascript %} 8 | 12 | 13 | 14 | 15 | 16 | 17 | {% endblock %} 18 | 19 | {% block content %} 20 |

21 | Welcome to Quick Start! 22 |

23 |

24 | This demo shows just how quick it is to start hundreds of instances. 25 |

26 | 27 |

28 | Edit your Compute Engine project 29 | information. 30 |

31 | 32 |

33 | 34 | 35 | 36 | Start 37 | Reset 38 |    39 |

40 | 41 |
42 | 43 | Timer: 44 |
45 |
46 | {% endblock %} 47 | -------------------------------------------------------------------------------- /demo-suite/static/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 60px; 3 | } 4 | 5 | h1 { 6 | margin-bottom: 20px; 7 | } 8 | 9 | .container-fluid { 10 | max-width: 1200px; 11 | margin-right: auto; 12 | margin-left: auto; 13 | } 14 | 15 | .btn-stack .btn { 16 | margin-bottom: 5px; 17 | margin-right: 10px; 18 | display: block; 19 | } 20 | 21 | .btn-stack hr { 22 | border-top: 2px solid #eee; 23 | margin-right: 10px; 24 | margin-top: 10px; 25 | margin-bottom: 10px; 26 | } 27 | 28 | .color-block { 29 | height: 28px; 30 | width: 28px; 31 | margin: 3px 3px 0 0; 32 | border: 1px solid black; 33 | line-height: 0px; 34 | display: inline-block; 35 | } 36 | 37 | .color-block [class^="icon-"], 38 | .color-block [class*=" icon-"] { 39 | vertical-align: middle; 40 | color: white; 41 | visibility: hidden; 42 | } 43 | 44 | .status-other { 45 | background-color: #666666; // Gray 46 | } 47 | 48 | .status-terminated { 49 | background-color: #666666; // Gray 50 | } 51 | 52 | .status-provisioning { 53 | background-color: #3369E8; // Blue 54 | } 55 | 56 | .status-staging { 57 | background-color: #EEB211; // Yellow 58 | } 59 | 60 | .status-running { 61 | background-color: #009925; // Green 62 | } 63 | 64 | .status-serving { 65 | background-color: #009925; // Green 66 | } 67 | 68 | .status-stopping { 69 | background-color: #D50F25; // Red 70 | } 71 | 72 | .status-stopped { 73 | background-color: #D50F25; // Red 74 | } 75 | 76 | .status-serving [class^="icon-"], 77 | .status-serving [class*=" icon-"] { 78 | visibility: visible; 79 | } 80 | -------------------------------------------------------------------------------- /demo-suite/main.py: -------------------------------------------------------------------------------- 1 | # Copyright 2012 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Main page for the Google Compute Engine demo suite.""" 16 | 17 | from __future__ import with_statement 18 | 19 | __author__ = 'kbrisbin@google.com (Kathryn Hurley)' 20 | 21 | import lib_path 22 | import google_cloud.oauth as oauth 23 | import jinja2 24 | import webapp2 25 | 26 | from google.appengine.api import users 27 | 28 | jinja_environment = jinja2.Environment(loader=jinja2.FileSystemLoader('')) 29 | decorator = oauth.decorator 30 | 31 | 32 | class Main(webapp2.RequestHandler): 33 | """Show the main page.""" 34 | 35 | def get(self): 36 | """Show the main page.""" 37 | template = jinja_environment.get_template('templates/index.html') 38 | logout_url = users.create_logout_url('/') 39 | self.response.out.write(template.render({'logout_url': logout_url})) 40 | 41 | 42 | app = webapp2.WSGIApplication( 43 | [ 44 | ('/', Main), 45 | (decorator.callback_path, decorator.callback_handler()), 46 | ], debug=True) 47 | -------------------------------------------------------------------------------- /demo-suite/lib_path.py: -------------------------------------------------------------------------------- 1 | # Copyright 2012 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Sets the sys path for the libraries within the folder lib.""" 16 | 17 | __author__ = 'kbrisbin@google.com (Kathryn Hurley)' 18 | 19 | import os 20 | import sys 21 | 22 | sys.path.append(os.path.join(os.path.dirname(__file__), 'lib')) 23 | 24 | sys.path.append(os.path.join(os.path.dirname(__file__), 25 | 'ext_lib', 26 | 'httplib2-0.8', 27 | 'python2')) 28 | 29 | sys.path.append(os.path.join(os.path.dirname(__file__), 30 | 'ext_lib', 31 | 'google-api-python-client-1.1')) 32 | 33 | sys.path.append(os.path.join(os.path.dirname(__file__), 34 | 'ext_lib', 35 | 'oauth2client-1.0')) 36 | 37 | sys.path.append(os.path.join(os.path.dirname(__file__), 38 | 'ext_lib', 39 | 'python-gflags-2.0')) 40 | -------------------------------------------------------------------------------- /demo-suite/demos/fractal/vm_files/map_test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | Mandlebrot Viewer 9 | 10 | 11 | 58 | 63 | 64 | 65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /demo-suite/demos/fractal/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "templates/base.html" %} 2 | 3 | {% block extra_css %} 4 | 5 | {% endblock %} 6 | 7 | {% block extra_javascript %} 8 | 11 | 12 | 13 | 14 | 15 | 16 | {% endblock %} 17 | 18 | {% block content %} 19 |
20 |
21 |

Fractals 22 | 23 |

24 |
25 |
26 |

Single Instance direct connect

27 |
28 |
29 |

Instance Cluster 30 | {% if lb_enabled %} 31 | using load balancer 32 | {% else %} 33 | direct connect 34 | {% endif %} 35 |

36 |
37 |
38 | 39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | 51 |
52 |
53 | Start VMs 54 | Stop VMs 55 |
56 | {% if lb_enabled %} 57 | Add a VM 58 | Kill a VM 59 |
60 | {% endif %} 61 | Clear Stats 62 |
63 | Show Maps 64 |
65 | Random POI 66 |
67 |
68 |
69 |
70 |
71 |
72 | 73 | {% endblock %} 74 | -------------------------------------------------------------------------------- /DESIGN.md: -------------------------------------------------------------------------------- 1 | # Design 2 | 3 | This AppEngine app is designed to provide visual demos for using the GCE API. 4 | 5 | ## Server Side 6 | Generally it consists of a python driver that launches and manages information. 7 | A simple GCE library is included in `lib/google_cloud/gce.py`. This is built on 8 | the [Google API Python Library][python-lib]. 9 | 10 | The discovery document is checked in (`demo- 11 | suite/discovery/compute/v1beta14.json`). When updating to a new version a new 12 | discovery doc will have to be fetched and used. An easy way to do this is to 13 | grab it from the gcutil tarball. 14 | 15 | Individual demo handlers should have a method that returns a JSON dictionary of 16 | instance state and data. It might look like this: 17 | 18 | ```JSON 19 | { 20 | "instances":{ 21 | "quick-start-3":{ 22 | "status":"PROVISIONING" 23 | }, 24 | "quick-start-2":{ 25 | "status":"STAGING" 26 | }, 27 | "quick-start-1":{ 28 | "status":"RUNNING" 29 | }, 30 | "quick-start-0":{ 31 | "status":"RUNNING" 32 | }, 33 | "quick-start-4":{ 34 | "status":"RUNNING" 35 | } 36 | }, 37 | } 38 | ``` 39 | 40 | There is a helper for doing common simple operations and generating this type of 41 | output. That is located at `lib/google_cloud/gce_appengine.py` 42 | 43 | ## Client Side 44 | 45 | There is a corresponding `gce.js` that helps to drive this stuff on the client. 46 | It knows how to start VMs, stop VMs and get status information. 47 | 48 | There are a set of `gceUi` objects that can be installed into a `Gce` object to 49 | receive update notifications. There are three methods that can be called on one 50 | of these objects: 51 | 52 | 1. `start()` -- Called when an operation is started. This includes creating 53 | and deleting instances. 54 | 2. `stop()` -- Called when the operation is completed. 55 | 3. `update(data)` -- Called when new information available. This includes the 56 | data structure above along with some extra information like a histogram of 57 | the number of VMs in each state. 58 | 59 | There are currently 3 UI widgets you can use: 60 | 61 | 1. `squares.js` -- Display a square for each VM. Great for showing the status 62 | of a cluster. 63 | 2. `counter.js` -- Show how many VMs are running. 64 | 3. `timer.js` -- Time the amount of time it takes for an operation to 65 | complete. 66 | 67 | [python-lib]: https://code.google.com/p/google-api-python-client/ 68 | -------------------------------------------------------------------------------- /demo-suite/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Google Compute Engine Demos! 6 | 7 | 8 | 9 | 10 | 11 | {% block extra_css %} 12 | {% endblock %} 13 | 14 | 15 | 18 | 19 | 22 | 24 | 26 | 28 | {% block extra_javascript %} 29 | {% endblock %} 30 | 31 | 32 | 67 |
68 | {% block content %} 69 | {% endblock %} 70 |
71 | 72 | 73 | -------------------------------------------------------------------------------- /demo-suite/static/js/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2012 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @fileoverview Various generic utilities . 17 | */ 18 | 19 | /** 20 | * Repeat a string n times. 21 | * @param {string} string The string to repeat. 22 | * @param {number} length The number of times to repeat. 23 | * @return {string} A string containing {@code length} repetitions of 24 | * {@code string}. 25 | */ 26 | stringRepeat = function(string, length) { 27 | return new Array(length + 1).join(string); 28 | } 29 | 30 | /** 31 | * Pads a number with preceeding zeros. 32 | * @param {number} num The number to pad. 33 | * @param {number} length The total string length to return. 34 | * @return {string} {@code num} as a string padded with zeros. 35 | */ 36 | padNumber = function(num, length) { 37 | var s = String(num); 38 | index = s.length; 39 | return stringRepeat('0', Math.max(0, length - index)) + s; 40 | }; 41 | 42 | /** 43 | * Determine if 2 maps (associative arrays) are equal. 44 | * @param {Object} a 45 | * @param {Object} b 46 | * @return {boolean} True if they are equal. 47 | */ 48 | mapsEqual = function(a, b) { 49 | if (a == b) { 50 | return true; 51 | } 52 | 53 | if (a == null || b == null) { 54 | return false; 55 | } 56 | 57 | if (Object.keys(a).length != Object.keys(b).length) { 58 | return false; 59 | } 60 | 61 | for (key in a) { 62 | if (a[key] != b[key]) { 63 | return false; 64 | } 65 | } 66 | 67 | return true; 68 | } 69 | 70 | /** 71 | * Compare two arrays to see if they are equal. 72 | * @param {Array} a 73 | * @param {Array} b 74 | * @return {boolean} true if a and b are equal. 75 | */ 76 | function arraysEqual(a, b) { 77 | if (a == b) { 78 | return true; 79 | } 80 | if (a == null || b == null) { 81 | return false; 82 | } 83 | 84 | if (a.length != b.length) { 85 | return false; 86 | } 87 | 88 | for (var i = 0; i < a.length; i++) { 89 | if (a[i] != b[i]) { 90 | return false; 91 | } 92 | } 93 | return true; 94 | } 95 | -------------------------------------------------------------------------------- /demo-suite/lib/google_cloud/gcs_appengine.py: -------------------------------------------------------------------------------- 1 | # Copyright 2012 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Gce App Engine helper methods to work with Compute Engine.""" 16 | 17 | __author__ = 'kbrisbin@google.com (Kathryn Hurley)' 18 | 19 | import cs 20 | import lib_path 21 | 22 | from google.appengine.ext import deferred 23 | 24 | 25 | class GcsAppEngineHelper(object): 26 | """Some helpful methods for working with Cloud Storage. 27 | 28 | Attributes: 29 | credentials: An oauth2client.client.Credentials object. 30 | project_id: A string name for the Cloud Storage project (this is a 31 | string of numbers). 32 | """ 33 | 34 | def __init__(self, credentials, project_id): 35 | """Initializes the GcsAppEngineHelper class. 36 | 37 | Sets default values for class attributes. 38 | 39 | Args: 40 | credentials: An oauth2client.client.Credentials object. 41 | project_id: A string name for the Cloud Storage project (this is a 42 | string of numbers). 43 | """ 44 | self.credentials = credentials 45 | self.project_id = project_id 46 | 47 | def delete_bucket_contents(self, bucket, directory=None, file_regex=None): 48 | """Deletes all the contents from a given bucket and directory path. 49 | 50 | Args: 51 | bucket: A string name of the Cloud Storage bucket. 52 | directory: A string name of the Cloud Storage 'directory'. 53 | file_regex: A regular expression to match against object names. 54 | """ 55 | deferred.defer(cleanup_queue, self.credentials, self.project_id, bucket, 56 | directory, file_regex) 57 | 58 | 59 | def cleanup_queue(credentials, project_id, bucket, directory, file_regex=None): 60 | """Deletes all the contents from a given bucket and directory path. 61 | 62 | Args: 63 | credentials: An oauth2client.client.Credentials object. 64 | project_id: A string name for the Cloud Storage project (this is a 65 | string of numbers). 66 | bucket: A string name of the Cloud Storage bucket. 67 | directory: A string name of the Cloud Storage 'directory'. 68 | file_regex: A regular expression to match against object names. 69 | """ 70 | cs.Cs(project_id).delete_bucket_contents( 71 | credentials.access_token, bucket, directory, file_regex) 72 | -------------------------------------------------------------------------------- /CONTRIB.md: -------------------------------------------------------------------------------- 1 | # How to become a contributor and submit your own code 2 | 3 | ## Contributor License Agreements 4 | 5 | We'd love to accept your sample apps and patches! Before we can take them, we have to jump a couple of legal hurdles. 6 | 7 | Please fill out either the individual or corporate Contributor License Agreement (CLA). 8 | 9 | * If you are an individual writing original source code and you're sure you own the intellectual property, then you'll need to sign an [individual CLA](http://code.google.com/legal/individual-cla-v1.0.html). 10 | * If you work for a company that wants to allow you to contribute your work, then you'll need to sign a [corporate CLA](http://code.google.com/legal/corporate-cla-v1.0.html). 11 | 12 | Follow either of the two links above to access the appropriate CLA and instructions for how to sign and return it. Once we receive it, we'll be able to accept your pull requests. 13 | 14 | ## Contributing A Patch 15 | 16 | 1. Submit an issue describing your proposed change to the repo in question. 17 | 1. The repo owner will respond to your issue promptly. 18 | 1. If your proposed change is accepted, and you haven't already done so, sign a Contributor License Agreement (see details above). 19 | 1. Fork the desired repo, develop and test your code changes. 20 | 1. Ensure that your code adheres to the existing style in the sample to which you are contributing. Refer to the [Google Cloud Platform Samples Style Guide](https://github.com/GoogleCloudPlatform/Template/wiki/style.html) for the recommended coding standards for this organization. 21 | 1. Ensure that your code has an appropriate set of unit tests which all pass. 22 | 1. Submit a pull request. 23 | 24 | ## Contributing A New Sample App 25 | 26 | 1. Submit an issue to the GoogleCloudPlatform/Template repo describing your proposed sample app. 27 | 1. The Template repo owner will respond to your enhancement issue promptly. Instructional value is the top priority when evaluating new app proposals for this collection of repos. 28 | 1. If your proposal is accepted, and you haven't already done so, sign a Contributor License Agreement (see details above). 29 | 1. Create your own repo for your app following this naming convention: 30 | * {product}-{app-name}-{language} 31 | * products: appengine, compute, storage, bigquery, prediction, cloudsql 32 | * example: appengine-guestbook-python 33 | * For multi-product apps, concatenate the primary products, like this: compute-appengine-demo-suite-python. 34 | * For multi-language apps, concatenate the primary languages like this: appengine-sockets-python-java-go. 35 | 1. Clone the README.md, CONTRIB.md and LICENSE files from the GoogleCloudPlatform/Template repo. 36 | 1. Ensure that your code adheres to the existing style in the sample to which you are contributing. Refer to the [Google Cloud Platform Samples Style Guide](https://github.com/GoogleCloudPlatform/Template/wiki/style.html) for the recommended coding standards for this organization. 37 | 1. Ensure that your code has an appropriate set of unit tests which all pass. 38 | 1. Submit a request to fork your repo in GoogleCloudPlatform organizationt via your proposal issue. 39 | -------------------------------------------------------------------------------- /demo-suite/static/bootstrap/js/bootstrap-button.js: -------------------------------------------------------------------------------- 1 | /* ============================================================ 2 | * bootstrap-button.js v2.0.4 3 | * http://twitter.github.com/bootstrap/javascript.html#buttons 4 | * ============================================================ 5 | * Copyright 2012 Twitter, Inc. 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * ============================================================ */ 19 | 20 | 21 | !function ($) { 22 | 23 | "use strict"; // jshint ;_; 24 | 25 | 26 | /* BUTTON PUBLIC CLASS DEFINITION 27 | * ============================== */ 28 | 29 | var Button = function (element, options) { 30 | this.$element = $(element) 31 | this.options = $.extend({}, $.fn.button.defaults, options) 32 | } 33 | 34 | Button.prototype.setState = function (state) { 35 | var d = 'disabled' 36 | , $el = this.$element 37 | , data = $el.data() 38 | , val = $el.is('input') ? 'val' : 'html' 39 | 40 | state = state + 'Text' 41 | data.resetText || $el.data('resetText', $el[val]()) 42 | 43 | $el[val](data[state] || this.options[state]) 44 | 45 | // push to event loop to allow forms to submit 46 | setTimeout(function () { 47 | state == 'loadingText' ? 48 | $el.addClass(d).attr(d, d) : 49 | $el.removeClass(d).removeAttr(d) 50 | }, 0) 51 | } 52 | 53 | Button.prototype.toggle = function () { 54 | var $parent = this.$element.parent('[data-toggle="buttons-radio"]') 55 | 56 | $parent && $parent 57 | .find('.active') 58 | .removeClass('active') 59 | 60 | this.$element.toggleClass('active') 61 | } 62 | 63 | 64 | /* BUTTON PLUGIN DEFINITION 65 | * ======================== */ 66 | 67 | $.fn.button = function (option) { 68 | return this.each(function () { 69 | var $this = $(this) 70 | , data = $this.data('button') 71 | , options = typeof option == 'object' && option 72 | if (!data) $this.data('button', (data = new Button(this, options))) 73 | if (option == 'toggle') data.toggle() 74 | else if (option) data.setState(option) 75 | }) 76 | } 77 | 78 | $.fn.button.defaults = { 79 | loadingText: 'loading...' 80 | } 81 | 82 | $.fn.button.Constructor = Button 83 | 84 | 85 | /* BUTTON DATA-API 86 | * =============== */ 87 | 88 | $(function () { 89 | $('body').on('click.button.data-api', '[data-toggle^=button]', function ( e ) { 90 | var $btn = $(e.target) 91 | if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') 92 | $btn.button('toggle') 93 | }) 94 | }) 95 | 96 | }(window.jQuery); 97 | -------------------------------------------------------------------------------- /demo-suite/static/js/timer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2012 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @fileoverview Timer display shows time formatted as 00:00:00.. 17 | * 18 | * Start and stop a timer. Timer increments every second. 19 | * 20 | */ 21 | 22 | /** 23 | * Timer class displays a timer in the given HTML element. 24 | * @constructor 25 | * @param {Element} container The HTML element in which to display the timer. 26 | */ 27 | var Timer = function(container) { 28 | this.container_ = container; 29 | this.container_.innerHTML = '00:00:00'; 30 | }; 31 | 32 | /** 33 | * The HTML element to display the timer. 34 | * @type {Element} 35 | * @private 36 | */ 37 | Timer.prototype.container_ = null; 38 | 39 | /** 40 | * The timer interval. 41 | * @type {Object} 42 | * @private 43 | */ 44 | Timer.prototype.timerInterval_ = null; 45 | 46 | /** 47 | * The timer interval time in milliseconds. 48 | * @type {number} 49 | * @private 50 | */ 51 | Timer.prototype.TIMER_INTERVAL_TIME_ = 1000; 52 | 53 | /** 54 | * The timer state. 55 | * @type {boolean} 56 | * @private 57 | */ 58 | Timer.prototype.running_ = false; 59 | 60 | /** 61 | * The elapsed seconds. 62 | * @type {number} 63 | * @private 64 | */ 65 | Timer.prototype.seconds_ = 0; 66 | 67 | 68 | /** 69 | * Start the timer. 70 | */ 71 | Timer.prototype.start = function() { 72 | var that = this; 73 | 74 | // Start timer if not already running. 75 | if (!Timer.prototype.running_) { 76 | this.timerInterval_ = setInterval(function() { 77 | that.tick_(); 78 | }, this.TIMER_INTERVAL_TIME_); 79 | this.tick_(); 80 | Timer.prototype.running_ = true; 81 | } 82 | }; 83 | 84 | /** 85 | * Stop the timer. 86 | */ 87 | Timer.prototype.stop = function() { 88 | this.seconds_ = 0; 89 | clearInterval(this.timerInterval_); 90 | Timer.prototype.running_ = false; 91 | }; 92 | 93 | /** 94 | * Set starting offset. 95 | */ 96 | Timer.prototype.setOffset = function(offset) { 97 | this.seconds_ = offset; 98 | } 99 | 100 | /** 101 | * Increment the timer every second. 102 | * @private 103 | */ 104 | Timer.prototype.tick_ = function() { 105 | var secs = this.seconds_++; 106 | var hrs = Math.floor(secs / 3600); 107 | secs %= 3600; 108 | var mns = Math.floor(secs / 60); 109 | secs %= 60; 110 | var pretty = (hrs < 10 ? '0' : '') + hrs + 111 | ':' + (mns < 10 ? '0' : '') + mns + 112 | ':' + (secs < 10 ? '0' : '') + secs; 113 | this.container_.innerHTML = pretty; 114 | }; 115 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![status: inactive](https://img.shields.io/badge/status-inactive-red.svg) 2 | 3 | This project is no longer actively developed or maintained. 4 | 5 | For more information about Compute Engine, refer to our [documentation](https://cloud.google.com/compute). 6 | 7 | # Google Compute Engine demo suite 8 | 9 | ## About 10 | 11 | The Compute Engine demo suite contains a variety of demos showing how 12 | to use Google Compute Engine. The demos are available live at 13 | [http://gce-demos.appspot.com][1]. 14 | 15 | If you would like to run the application locally, follow the setup 16 | instructions. 17 | 18 | ## Setup Instructions 19 | 20 | 1. Update the application value in the root `app.yaml` file to your own 21 | App Engine app identity. 22 | 23 | application: your-app-id 24 | 25 | More information about the app.yaml file can be found in the [App 26 | Engine documentation][2]. 27 | 28 | 2. Add a `client_secrets.json` file within the `lib/google_cloud` directory 29 | with your client id and secrets, as found in the API console. The file 30 | should look something like this: 31 | 32 |
{
 33 |      "web": {
 34 |        "client_id": "24043....apps.googleusercontent.com",
 35 |        "client_secret": "iPVXC5...xVz",
 36 |        "redirect_uris": ["http://localhost:8080/oauth2callback",
 37 |                          "http://<your-app-id>.appspot.com/oauth2callback"],
 38 |        "auth_uri": "https://accounts.google.com/o/oauth2/auth",
 39 |        "token_uri": "https://accounts.google.com/o/oauth2/token"
 40 |      }
 41 |    }
42 | 43 | Also make sure that the redirect URIs are correctly associated with the 44 | client id and secret in the API console. 45 | 46 | More information about client secrets can be found in the 47 | [API client library documentation][3]. 48 | 49 | 3. (optional) Update any of the defaults in the settings.json to 50 | match your preferences. 51 | 52 | 4. (optional) You can optionally create custom images for the Fractal and 53 | Image Magick demos that will allow the instances to start quicker. First, 54 | start the instances using the demo UI. When at least one of the instances 55 | is up and running, ssh into that instance and follow the directions 56 | [here][7] for creating an image for an instance. 57 | 58 | Name the images `fractal-demo-image` and `image-magick-demo-image` 59 | respectively. 60 | 61 | 5. Install dependencies listed in the dependencies section into the `ext_lib` 62 | directory. You can do this easily by executing the 63 | `download_dependencies.sh` bash script. Beware that this will delete all 64 | current contents of the `ext_lib` dir and download the dependencies fresh. 65 | 66 | ## Dependencies 67 | 68 | Add to `ext_lib` directory: 69 | 70 | - [python_gflags-2.0][8] 71 | - [httplib2-0.8][9] 72 | - [oauth2client-1.0][10] 73 | - [google-api-python-client][11] 74 | 75 | When adding new dependencies do the following: 76 | 77 | 1. Add them to the list here 78 | 2. Add them to the `download_dependencies.sh` script. 79 | 3. Add them to `demo-suite/lib_path.py` 80 | 81 | ## Fractal Demo 82 | 83 | ### Load Balancing 84 | The fractal demo can use load balancing. However, the feature is in preview and the API is under active development. As such, there are some pieces missing that will be filled in as the feature reaches maturity. 85 | 86 | If load balancing **is** set up, it will work to forward all connections to an IP address to a set of VMs with a specific tag (fractal-cluster). Currently, the projects that support this are hard coded in the `demo-suite/demos/fractal/main.py` along with the IP/hostnames for the load balancer. 87 | 88 | ### Boot from PD 89 | If you initialize a set of boot PDs, they will be detected and used instead of booting from scratch disks. To do this run the `demo-suite/demos/fractal/createpds.sh` script. You'll have to update it to point to your project. 90 | 91 | 92 | [1]: http://gce-demos.appspot.com 93 | [2]: https://developers.google.com/appengine/docs/python/config/appconfig#About_app_yaml 94 | [3]: https://developers.google.com/api-client-library/python/guide/aaa_client_secrets 95 | [4]: https://developers.google.com/api-client-library/python/platforms/google_app_engine#ServiceAccounts 96 | [5]: https://developers.google.com/storage/ 97 | [6]: https://developers.google.com/compute/docs/faq#wherecanifind 98 | [7]: https://developers.google.com/compute/docs/images#installinganimage 99 | [8]: http://code.google.com/p/python-gflags/ 100 | [9]: http://code.google.com/p/httplib2/ 101 | [10]: http://pypi.python.org/pypi/oauth2client/1.0 102 | [11]: https://code.google.com/p/google-api-python-client/ 103 | -------------------------------------------------------------------------------- /demo-suite/demos/fractal/static/js/throttled_image_map.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @fileoverview A throttlered image map. 17 | */ 18 | 19 | /** 20 | * A MapType object that throttles image requests 21 | * @param {Object} opts 22 | */ 23 | var ThrottledImageMap = function(opts) { 24 | this.alt = opts['alt']; 25 | this.tileSize = opts['tileSize']; 26 | this.name = opts['name']; 27 | this.minZoom = opts['minZoom']; 28 | this.maxZoom = opts['maxZoom']; 29 | this.maxDownloading = opts['maxDownloading'] || 5; 30 | this.getTileUrl = opts['getTileUrl']; 31 | 32 | // All tiles that we know about. These might be loading, loaded or queued. 33 | this.tiles = {}; 34 | 35 | // All tiles that are current loading. 36 | this.loadingTiles = {}; 37 | 38 | // The loadQueue is an ordered list of tiles to load. If a tile is no longer 39 | // needed, it will be removed from this.tiles but not from loadQueue. So when 40 | // processing the queue, we must first check to see if the tile is still 41 | // needed by also checking this.tiles. 42 | this.loadQueue = []; 43 | } 44 | 45 | ThrottledImageMap.prototype.getTile = function(tileCoord, zoom, ownerDocument) { 46 | var tileUrl = this.getTileUrl(tileCoord, zoom); 47 | 48 | if (tileUrl in this.tiles) { 49 | console.log('Returning existing tile: ' + tileUrl); 50 | return this.tiles[tileUrl]; 51 | } 52 | 53 | var tileDiv = ownerDocument.createElement('div'); 54 | var tileId = 'tile-' + String(Math.floor( Math.random()*999999)); 55 | tileDiv.id = tileId; 56 | tileDiv.tileUrl = tileUrl; 57 | tileDiv.style.width = this.tileSize.width + 'px'; 58 | tileDiv.style.height = this.tileSize.height + 'px'; 59 | 60 | this.tiles[tileId] = tileDiv; 61 | 62 | this.addTileToQueue_(tileDiv); 63 | this.processQueue_(); 64 | 65 | return tileDiv; 66 | }; 67 | 68 | ThrottledImageMap.prototype.releaseTile = function(tileDiv) { 69 | var tileId = tileDiv.id; 70 | if (tileId in this.tiles) { 71 | divFromMap = this.tiles[tileId]; 72 | if (divFromMap !== tileDiv) { 73 | console.log('Error: tile release doesn\'t match tile being loaded: ' 74 | + tileUrl); 75 | console.log(' releasedTile: ', tileDiv); 76 | console.log(' tileFromMap: ', divFromMap); 77 | } 78 | console.log('Releasing tile: ' + tileId + ' url: ' + tileDiv.tileUrl) 79 | delete this.tiles[tileId]; 80 | 81 | $(tileDiv).empty(); 82 | } 83 | }; 84 | 85 | ThrottledImageMap.prototype.addTileToQueue_ = function(tileDiv) { 86 | console.log('Queuing load of tile: ' + tileDiv.tileUrl); 87 | this.loadQueue.push(tileDiv); 88 | }; 89 | 90 | ThrottledImageMap.prototype.processQueue_ = function() { 91 | while (this.loadQueue.length > 0 && Object.keys(this.loadingTiles).length < this.maxDownloading) { 92 | var tileDiv = this.loadQueue.shift(); 93 | var tileUrl = tileDiv.tileUrl; 94 | var tileId = tileDiv.id; 95 | 96 | if (!(tileId in this.tiles)) { 97 | // This tile is no longer needed so just forget about it and continue. 98 | console.log('Ignoring no longer needed: ' + tileId); 99 | continue; 100 | } 101 | 102 | var img = tileDiv.ownerDocument.createElement('img'); 103 | img.style.width = this.tileSize.width + 'px'; 104 | img.style.height = this.tileSize.height + 'px'; 105 | img.onload = this.onImageLoaded_.bind(this, tileId, tileUrl); 106 | img.onerror = this.onImageError_.bind(this, tileId, tileUrl); 107 | console.log('Loading tile: ' + tileUrl); 108 | img.src = tileUrl; 109 | tileDiv.appendChild(img); 110 | this.loadingTiles[tileId] = tileDiv; 111 | } 112 | }; 113 | 114 | ThrottledImageMap.prototype.onImageLoaded_ = function(tileId, tileUrl) { 115 | console.log('Tile loaded: ' + tileId + ' url: ' + tileUrl); 116 | delete this.loadingTiles[tileId]; 117 | this.processQueue_(); 118 | }; 119 | 120 | ThrottledImageMap.prototype.onImageError_ = function(tileId, tileUrl) { 121 | console.log('Tile error: ' + tileId + ' url: ' + tileUrl); 122 | delete this.loadingTiles[tileId]; 123 | this.processQueue_(); 124 | }; 125 | -------------------------------------------------------------------------------- /demo-suite/lib/google_cloud/cs.py: -------------------------------------------------------------------------------- 1 | # Copyright 2012 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Cloud Storage library for uploading and deleting files.""" 16 | 17 | __author__ = 'kbrisbin@google.com (Kathryn Hurley)' 18 | 19 | import datetime 20 | import logging 21 | import re 22 | from xml.dom import minidom 23 | 24 | from google.appengine.api import urlfetch 25 | 26 | BASE_URL = 'https://storage.googleapis.com' 27 | API_VERSION = '2' 28 | 29 | 30 | class Cs(object): 31 | """Cloud Storage library. 32 | 33 | Attributes: 34 | project_id: A string name for the Cloud Storage project (this is a 35 | string of numbers). 36 | """ 37 | 38 | def __init__(self, project_id): 39 | """Initializes the Cs object. 40 | 41 | Args: 42 | project_id: A string name for the Cloud Storage project (this is a 43 | string of numbers). 44 | """ 45 | self.project_id = project_id 46 | 47 | def upload(self, oauth_token, bucket, object_name, payload, 48 | content_type='text/plain'): 49 | """Uploads an object to Cloud Storage in the given bucket. 50 | 51 | Args: 52 | oauth_token: String oauth token for sending authorized requests. 53 | bucket: String name of bucket in which to upload file. 54 | object_name: String name of the object. 55 | payload: File contents. 56 | content_type: String name describing the content type. 57 | Returns: 58 | The string result of the API call. 59 | """ 60 | # TODO(kbrisbin): This hasn't been tested yet. 61 | url = '%s/%s/%s' % (BASE_URL, bucket, object_name) 62 | date = datetime.datetime.now() 63 | str_date = date.strftime('%b %d, %Y %H:%M:%S') 64 | result = urlfetch.fetch( 65 | url=url, payload=payload, method=urlfetch.PUT, 66 | headers={ 67 | 'Authorization': 'OAuth %s' % (oauth_token), 68 | 'Date': str_date, 69 | 'x-goog-project-id': self.project_id, 70 | 'x-goog-api-version': API_VERSION, 71 | 'Content-Type': content_type}) 72 | return result.content 73 | 74 | def delete_bucket_contents(self, oauth_token, bucket, directory=None, 75 | file_regex=None): 76 | """Deletes all the contents of a given bucket / directory. 77 | 78 | Args: 79 | oauth_token: String oauth token for sending authorized requests. 80 | bucket: String name of bucket in which to upload file. 81 | directory: A symbolic directory from which to delete objects. 82 | file_regex: A regular expression to match against object names. 83 | """ 84 | url = '%s/%s' % (BASE_URL, bucket) 85 | if directory: 86 | url = '%s?prefix=%s/' % (url, directory) 87 | logging.info('Deleting files from: ' + url) 88 | date = datetime.datetime.now() 89 | str_date = date.strftime('%b %d, %Y %H:%M:%S') 90 | result = urlfetch.fetch( 91 | url=url, headers={ 92 | 'Authorization': 'OAuth %s' % (oauth_token), 93 | 'Date': str_date, 94 | 'x-goog-project-id': self.project_id, 95 | 'x-goog-api-version': API_VERSION}) 96 | dom = minidom.parseString(result.content) 97 | keys = dom.getElementsByTagName('Key') 98 | for key_element in keys: 99 | key = self._get_text(key_element.childNodes) 100 | if file_regex and not re.match(file_regex, key): 101 | continue 102 | url = '%s/%s/%s' % (BASE_URL, bucket, key) 103 | logging.info('Deleting: %s', url) 104 | result = urlfetch.fetch( 105 | url=url, method=urlfetch.DELETE, headers={ 106 | 'Authorization': 'OAuth %s' % (oauth_token), 107 | 'Date': str_date, 108 | 'x-goog-project-id': self.project_id, 109 | 'x-goog-api-version': API_VERSION}) 110 | 111 | def _get_text(self, nodes): 112 | """Concatenates the text from several XML nodes. 113 | 114 | Args: 115 | nodes: List of XML nodes. 116 | 117 | Returns: 118 | A string of concatenated text nodes. 119 | """ 120 | rc = [] 121 | for node in nodes: 122 | if node.nodeType == node.TEXT_NODE: 123 | rc.append(node.data) 124 | return ''.join(rc) 125 | -------------------------------------------------------------------------------- /demo-suite/demos/image-magick/static/js/script.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Image Magick demo JavaScript code. 3 | * 4 | * Displays status of instances in colored blocks, then displays processed 5 | * images uploaded to Cloud Storage from Compute Engine instances. 6 | */ 7 | 8 | $(document).ready(function() { 9 | var imageMagick = new ImageMagick(); 10 | imageMagick.initialize(); 11 | }); 12 | 13 | /** 14 | * Image Magick class. 15 | * @constructor 16 | */ 17 | var ImageMagick = function() { }; 18 | 19 | /** 20 | * The interval to ping Cloud Storage for processed images. 21 | * @type {Object} 22 | * @private 23 | */ 24 | ImageMagick.prototype.imageInterval_ = null; 25 | 26 | /** 27 | * The time between pings to Cloud Storage. 28 | * @type {number} 29 | * @private 30 | */ 31 | ImageMagick.prototype.IMAGE_INTERVAL_TIME_ = 2000; 32 | 33 | /** 34 | * The number of instances to start. 35 | * @type {number} 36 | * @private 37 | */ 38 | ImageMagick.prototype.NUM_INSTANCES_ = 50; 39 | 40 | /** 41 | * The Cloud Storage URL where image output is saved. 42 | * @type {string} 43 | * @private 44 | */ 45 | ImageMagick.prototype.CS_URL_ = 46 | 'http://' + BUCKET + '.storage.googleapis.com'; 47 | 48 | /** 49 | * The URL that deletes CS objects. 50 | * @type {string} 51 | * @private 52 | */ 53 | ImageMagick.prototype.CLEAN_CS_URL_ = '/' + DEMO_NAME + '/gcs-cleanup'; 54 | 55 | /** 56 | * Initialize the UI and check if there are instances already up. 57 | */ 58 | ImageMagick.prototype.initialize = function() { 59 | var instanceNames = []; 60 | for (var i = 0; i < this.NUM_INSTANCES_; i++) { 61 | instanceNames.push(DEMO_NAME + '-' + i); 62 | } 63 | var squares = new Squares( 64 | document.getElementById('instances'), instanceNames, { 65 | cols: 25 66 | }); 67 | squares.drawSquares(); 68 | 69 | var gce = new Gce('/' + DEMO_NAME + '/instance', 70 | '/' + DEMO_NAME + '/instance', 71 | '/' + DEMO_NAME + '/gce-cleanup', { 72 | squares: squares 73 | }); 74 | gce.getInstanceStates(function(data) { 75 | if (data['stateCount']['TOTAL'] != 0) { 76 | $('#start').addClass('disabled'); 77 | $('#reset').removeClass('disabled'); 78 | alert('Some instances are already running! Hit reset.'); 79 | } 80 | }); 81 | this.initializeButtons_(gce, squares); 82 | }; 83 | 84 | /** 85 | * Initialize UI controls. 86 | * @param {Object} gce Instance of Gce class. 87 | * @param {Object} squares Instance of the Squares class. 88 | * @private 89 | */ 90 | ImageMagick.prototype.initializeButtons_ = function(gce, squares) { 91 | $('.btn').button(); 92 | 93 | var that = this; 94 | $('#start').click(function() { 95 | $('#start').addClass('disabled'); 96 | 97 | gce.startInstances(that.NUM_INSTANCES_, { 98 | data: {'num_instances': that.NUM_INSTANCES_}, 99 | callback: function() { 100 | that.imageInterval_ = setInterval(function() { 101 | that.displayImages_(squares); 102 | }, that.IMAGE_INTERVAL_TIME_); 103 | } 104 | }); 105 | }); 106 | 107 | $('#reset').click(function() { 108 | // Remove squares and display shut down message. 109 | $(document.getElementById('instances')).empty(); 110 | $(document.getElementById('instances')).html('...shutting down...'); 111 | if (that.imageInterval_) { 112 | clearInterval(that.imageInterval_); 113 | } 114 | 115 | // Stop instances and remove Cloud Storage contents. 116 | gce.stopInstances(function() { 117 | $('#start').removeClass('disabled'); 118 | $('#reset').addClass('disabled'); 119 | $(document.getElementById('instances')).empty(); 120 | squares.drawSquares(); 121 | }); 122 | $.ajax({ 123 | type: 'POST', 124 | url: that.CLEAN_CS_URL_ 125 | }); 126 | }); 127 | }; 128 | 129 | /** 130 | * Ping Cloud Storage to get processed images. 131 | * @param {Object} squares Instance of the Squares class. 132 | * @private 133 | */ 134 | ImageMagick.prototype.displayImages_ = function(squares) { 135 | var that = this; 136 | var url = this.CS_URL_; 137 | if (DIRECTORY) { 138 | url += '?prefix=' + DIRECTORY + '/'; 139 | } 140 | $.ajax({ 141 | url: url, 142 | dataType: 'xml', 143 | success: function(xml) { 144 | var imageCount = 0; 145 | $(xml).find('Contents').each(function() { 146 | var imagePath = $(this).find('Key').text(); 147 | // Key = output/.png 148 | var instanceName = imagePath.replace('.gif', ''); 149 | if (DIRECTORY) { 150 | instanceName = instanceName.replace(DIRECTORY + '/', ''); 151 | } 152 | 153 | // If the image hasn't been added, add it. 154 | square = squares.getSquareDiv(instanceName); 155 | if (square.find('img').length < 1) { 156 | square.empty(); 157 | img = $('').attr('src', that.CS_URL_ + '/' + imagePath); 158 | square.append(img); 159 | } 160 | 161 | imageCount++; 162 | if (imageCount == that.NUM_INSTANCES_) { 163 | clearInterval(that.imageInterval_); 164 | $('#reset').removeClass('disabled'); 165 | } 166 | }); 167 | } 168 | }); 169 | }; 170 | -------------------------------------------------------------------------------- /demo-suite/demos/quick-start/static/js/script.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Quick Start JavaScript. 3 | * 4 | * Initializes instances, updates UI display to show running time and number 5 | * of running instances. Stops running instances. 6 | */ 7 | 8 | $(document).ready(function() { 9 | var quickStart = new QuickStart(); 10 | quickStart.initialize(); 11 | }); 12 | 13 | /** 14 | * Quick Start class. 15 | * @constructor 16 | */ 17 | var QuickStart = function() { }; 18 | 19 | // Recovery mode flag, initialized to false. 20 | var Recovering = false; 21 | 22 | // Starting and resetting notification strings. 23 | var STARTING = 'Starting...'; 24 | var RESETTING = 'Resetting...'; 25 | 26 | /** 27 | * Initialize the UI and check if there are instances already up. 28 | */ 29 | QuickStart.prototype.initialize = function() { 30 | var gce = new Gce('/' + DEMO_NAME + '/instance', 31 | '/' + DEMO_NAME + '/instance', 32 | '/' + DEMO_NAME + '/cleanup'); 33 | 34 | gce.getInstanceStates(function(data) { 35 | var numInstances = parseInt($('#num-instances').val(), 10); 36 | var currentInstances = data['stateCount']['TOTAL']; 37 | if (currentInstances != 0) { 38 | // Instances are already running so we're in recovery mode. Calculate 39 | // current elapsed time and set timer element accordingly. 40 | var startTime = parseInt($('#start-time').val(), 10); 41 | var currentTime = Math.round(new Date().getTime() / 1000) 42 | var elapsedTime = currentTime - startTime; 43 | Timer.prototype.setOffset(elapsedTime); 44 | 45 | // In order to draw grid, maintain counter and timer, and start 46 | // status polling, we simulate start click with number of instances 47 | // last started, but we set Recovering flag to true to inhibit 48 | // sending of start request to GCE. 49 | Recovering = true; 50 | $('#start').click(); 51 | var targetInstances = parseInt($('#target-instances').val(), 10); 52 | if (targetInstances == 0) { 53 | $('#reset').click(); 54 | } 55 | 56 | // In recovery mode, resets are ok but don't let user resend start, 57 | // because duplicate starts can cause confusion and perf problems. 58 | $('#start').addClass('disabled'); 59 | $('#reset').removeClass('disabled'); 60 | } 61 | }); 62 | 63 | this.counter_ = new Counter(document.getElementById('counter'), 'numRunning'); 64 | this.timer_ = new Timer(document.getElementById('timer')); 65 | this.initializeButtons_(gce); 66 | }; 67 | 68 | /** 69 | * Initialize UI controls. 70 | * @param {Object} gce Instance of Gce class. 71 | * @private 72 | */ 73 | QuickStart.prototype.initializeButtons_ = function(gce) { 74 | $('.btn').button(); 75 | 76 | var that = this; 77 | $('#start').click(function() { 78 | // Get the number of instances entered by the user. 79 | var numInstances = parseInt($('#num-instances').val(), 10); 80 | if (numInstances > 1000) { 81 | alert('Max instances is 1000, starting 1000 instead.'); 82 | numInstances = 1000; 83 | } else if (numInstances < 0) { 84 | alert('At least one instance needs to be started.'); 85 | return; 86 | } else if (numInstances === 0) { 87 | return; 88 | } 89 | 90 | // Start requested, disable start button but allow reset while starting. 91 | $('#start').addClass('disabled'); 92 | $('#reset').removeClass('disabled'); 93 | $('#in-progress').text(STARTING); 94 | 95 | var instanceNames = []; 96 | for (var i = 0; i < numInstances; i++) { 97 | instanceNames.push(DEMO_ID + '-' + i); 98 | } 99 | 100 | // Initialize the squares, set the Gce options, and start the instances. 101 | var squares = new Squares( 102 | document.getElementById('instances'), instanceNames, { 103 | drawOnStart: true 104 | }); 105 | that.counter_.targetState = 'RUNNING'; 106 | gce.setOptions({ 107 | squares: squares, 108 | counter: that.counter_, 109 | timer: that.timer_ 110 | }); 111 | gce.startInstances(numInstances, { 112 | data: {'num_instances': numInstances}, 113 | callback: function() { 114 | // Start completed, start button should already be disabled, and 115 | // reset button should already be enabled. 116 | $('#in-progress').text(''); 117 | if (Recovering) { 118 | Recovering = false; 119 | } 120 | } 121 | }); 122 | }); 123 | 124 | // Initialize reset button click event to stop instances. 125 | $('#reset').click(function() { 126 | that.counter_.targetState = 'TOTAL'; 127 | //$('#num-instances').val(0); 128 | // Reset requested, disable start button but allow resending of reset. 129 | $('#start').addClass('disabled'); 130 | $('#reset').removeClass('disabled'); 131 | $('#in-progress').text(RESETTING); 132 | gce.stopInstances(function() { 133 | // Reset completed, allow start button and disallow reset button. 134 | $('#start').removeClass('disabled'); 135 | $('#reset').addClass('disabled'); 136 | $('#in-progress').text(''); 137 | if (Recovering) { 138 | Recovering = false; 139 | } 140 | }); 141 | }); 142 | }; 143 | -------------------------------------------------------------------------------- /demo-suite/lib/google_cloud/gce_appengine.py: -------------------------------------------------------------------------------- 1 | # Copyright 2012 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """GCE App Engine Helper class.""" 16 | 17 | import json 18 | import logging 19 | 20 | from google.appengine.api import users 21 | import gce_exception as error 22 | 23 | 24 | MAX_RESULTS = 100 25 | 26 | class GceAppEngine(object): 27 | """Contains generic GCE methods for demos.""" 28 | 29 | def list_demo_instances(self, request_handler, gce_project, demo_name): 30 | """Retrieves instance list for the demo. 31 | 32 | Sends the instance list in the response as a JSON object, mapping instance 33 | name to status. 34 | 35 | Args: 36 | request_handler: An instance of webapp2.RequestHandler. 37 | gce_project: An object of type gce.GceProject. 38 | demo_name: The string name of the demo. 39 | """ 40 | 41 | instances = self.run_gce_request( 42 | request_handler, 43 | gce_project.list_instances, 44 | 'Error listing instances: ', 45 | filter='name eq ^%s.*' % demo_name, 46 | maxResults=MAX_RESULTS) 47 | 48 | instance_dict = {} 49 | for instance in instances: 50 | instance_dict[instance.name] = {'status': instance.status} 51 | 52 | result_dict = { 53 | 'instances': instance_dict, 54 | } 55 | request_handler.response.headers['Content-Type'] = 'application/json' 56 | request_handler.response.out.write(json.dumps(result_dict)) 57 | 58 | def delete_demo_instances(self, request_handler, gce_project, demo_name): 59 | """Deletes instances for the demo. 60 | 61 | First retrieves an instance list with instance names starting with the 62 | demo name. A bulk request is then sent to delete all these instances. 63 | 64 | Args: 65 | request_handler: An instance of webapp2.RequestHandler. 66 | gce_project: An object of type gce.GceProject. 67 | demo_name: The string name of the demo. 68 | """ 69 | 70 | instances = self.run_gce_request( 71 | request_handler, 72 | gce_project.list_instances, 73 | 'Error listing instances: ', 74 | filter='name eq ^%s-.*' % demo_name, 75 | maxResults=MAX_RESULTS) 76 | 77 | if instances: 78 | response = self.run_gce_request( 79 | request_handler, 80 | gce_project.bulk_delete, 81 | 'Error deleting instances: ', 82 | resources=instances) 83 | 84 | if response: 85 | self.response.headers['Content-Type'] = 'text/plain' 86 | self.response.out.write('stopping cluster') 87 | 88 | def list_demo_routes(self, request_handler, gce_project, route_name): 89 | """Retrieves route list for the demo. 90 | 91 | Sends the route list in the response as a JSON object 92 | 93 | Args: 94 | request_handler: An instance of webapp2.RequestHandler. 95 | gce_project: An object of type gce.GceProject. 96 | route_name: The string name of the route. 97 | """ 98 | 99 | routes = self.run_gce_request( 100 | request_handler, 101 | gce_project.list_routes, 102 | 'Error listing routes: ', 103 | filter='name eq ^%s$' % route_name, 104 | maxResults = MAX_RESULTS) 105 | 106 | return routes 107 | 108 | def delete_demo_route(self, request_handler, gce_project, route_name): 109 | """Deletes route for the demo. 110 | 111 | First retrieves an route list with route name matching the demo 112 | route. A bulk request is then sent to delete the resource. 113 | 114 | Args: 115 | request_handler: An instance of webapp2.RequestHandler. 116 | gce_project: An object of type gce.GceProject. 117 | route_name: The string name of the route. 118 | """ 119 | 120 | routes = self.run_gce_request( 121 | request_handler, 122 | gce_project.list_routes, 123 | 'Error listing routes: ', 124 | filter='name eq ^%s$' % route_name, 125 | maxResults=MAX_RESULTS) 126 | 127 | if routes: 128 | response = self.run_gce_request( 129 | request_handler, 130 | gce_project.bulk_delete, 131 | 'Error deleting instances', 132 | resources=routes) 133 | 134 | if response: 135 | self.response.headers['Content-Type'] = 'text/plain' 136 | self.response.out.write('Deleting route') 137 | 138 | def run_gce_request(self, request_handler, gce_method, error_message, **args): 139 | """Run a GCE Project list, insert, delete method. 140 | 141 | Any extra args are used as arguments for the gce_method. 142 | 143 | Args: 144 | request_handler: An instance of webapp2.RequestHandler. 145 | gce_method: A method within gce.GceProject to run. 146 | error_message: A string error message to prepend an error message, 147 | should an error occur. 148 | 149 | Returns: 150 | The response object, if the API call was successful. 151 | """ 152 | 153 | response = None 154 | try: 155 | response = gce_method(**args) 156 | except error.GceError, e: 157 | logging.error(error_message + e.message) 158 | request_handler.response.set_status(500, error_message + e.message) 159 | return 160 | except error.GceTokenError: 161 | request_handler.redirect(users.create_login_url(request_handler.uri)) 162 | return 163 | return response 164 | -------------------------------------------------------------------------------- /demo-suite/static/js/squares.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2012 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @fileoverview Display status squares for instances. 17 | * 18 | * Creates color block and updates the colors according to instance status. 19 | * 20 | */ 21 | 22 | /** 23 | * The Squares class controls the color blocks representing instance statuses 24 | * in the given HTML container. Each block is given an ID equal to the instance 25 | * name, using the given instanceNames. 26 | * Optional options are available to customize the squares including: 27 | * 37 | * @constructor 38 | * @param {Element} container HTML element in which to display the squares. 39 | * @param {Array.} instanceNames List of instance names. 40 | * @param {Object} squareOptions Options for the square (optional). 41 | */ 42 | var Squares = function(container, instanceNames, squareOptions) { 43 | /** 44 | * Container for the squares. 45 | * @type {JQuery} 46 | * @private 47 | */ 48 | this.container_ = $(container); 49 | 50 | /** 51 | * The number of columns in the UI display. If this is null a value is chosen 52 | * automatically. 53 | * @type {number} 54 | * @private 55 | */ 56 | this.numCols_ = null; 57 | 58 | /** 59 | * The default status colors. These are just classNames and can be customized 60 | * using the squareOptions object during initialization. 61 | * @type {Object} 62 | * @private 63 | */ 64 | this.statusClasses_ = null; 65 | 66 | /** 67 | * The string of instance names. 68 | * @type {Array.} 69 | * @private 70 | */ 71 | this.instanceNames_ = instanceNames; 72 | 73 | /** 74 | * If drawOnStart is true, this variable is set equal to the this.drawSquares 75 | * function. When a Square object is passed as a UI option to the Gce class, 76 | * the Gce class will call the start method in the startInstances function. 77 | * @type {Function} 78 | */ 79 | this.start = null; 80 | 81 | /** 82 | * A map from the instance name to the JQuery object representing that 83 | * instance. 84 | * @type {Object} 85 | * @private 86 | */ 87 | this.squares_ = {}; 88 | 89 | if (squareOptions.statusClasses) { 90 | this.statusClasses_ = squareOptions.statusClasses; 91 | } else { 92 | this.statusClasses_ = { 93 | 'OTHER': 'status-other', 94 | 'TERMINATED': 'status-terminated', 95 | 'PROVISIONING': 'status-provisioning', 96 | 'STAGING': 'status-staging', 97 | 'RUNNING': 'status-running', 98 | 'SERVING': 'status-serving', 99 | 'STOPPING': 'status-stopping', 100 | 'STOPPED': 'status-stopped', 101 | }; 102 | } 103 | if (squareOptions.drawOnStart) { 104 | this.start = this.drawSquares; 105 | } 106 | 107 | // If the num of cols is not set, create up to 25 cols based on the 108 | // number of instances. 109 | if (squareOptions.cols) { 110 | this.numCols_ = squareOptions.cols; 111 | } 112 | }; 113 | 114 | /** 115 | * Set in a new set of instance names. This will clear the display requiring 116 | * the user to draw the squares again and update data. 117 | * @param {Array} instance_names The list of instance names. 118 | */ 119 | Squares.prototype.resetInstanceNames = function(instance_names) { 120 | this.reset(); 121 | this.instanceNames_ = instance_names; 122 | }; 123 | 124 | /** 125 | * Returns the instance names 126 | * @return {Array} Returns the instance names. 127 | */ 128 | Squares.prototype.getInstanceNames = function() { 129 | return this.instanceNames_.slice(); 130 | }; 131 | 132 | /** 133 | * Draws the squares on the HTML page. 134 | */ 135 | Squares.prototype.drawSquares = function() { 136 | // First, clean up any old instance squares. 137 | this.reset(); 138 | 139 | 140 | // Add the color squares. 141 | for (var i = 0; i < this.instanceNames_.length; i++) { 142 | // TAG is defined in the html file as a template variable 143 | var instanceName = this.instanceNames_[i]; 144 | square = $('
') 145 | .addClass('color-block') 146 | .addClass(this.statusClasses_['OTHER']) 147 | .append(''); 148 | this.container_.append(square); 149 | this.squares_[instanceName] = square; 150 | 151 | if ((i+1) % this.numCols_ == 0) { 152 | $('
').appendTo(this.container_); 153 | } 154 | } 155 | }; 156 | 157 | /** 158 | * Get the number of columns to use. 159 | * @return {Number} The number of columns to display. 160 | */ 161 | Squares.prototype.getNumCols_ = function() { 162 | var numInstances = this.instanceNames_.length; 163 | var numCols = this.numCols_; 164 | if (!numCols) { 165 | numCols = Math.ceil(Math.sqrt(numInstances)); 166 | if (numCols > 25) { 167 | numCols = 25; 168 | } 169 | } 170 | return numCols; 171 | }; 172 | 173 | /** 174 | * Changes the color of the squares according to the instance status. Called 175 | * during the Gce.heartbeat. 176 | * @param {Object} updateData The status data returned from the server. 177 | */ 178 | Squares.prototype.update = function(updateData) { 179 | var instanceStatus = updateData['instances'] || {}; 180 | for (var i = 0; i < this.instanceNames_.length; i++) { 181 | var instanceName = this.instanceNames_[i]; 182 | var statusClass = null; 183 | if (instanceStatus.hasOwnProperty(instanceName)) { 184 | var status = instanceStatus[instanceName]['status']; 185 | statusClass = this.statusClasses_[status]; 186 | if (!statusClass) { 187 | statusClass = this.statusClasses_['OTHER']; 188 | } 189 | } else { 190 | statusClass = this.statusClasses_['TERMINATED']; 191 | } 192 | this.setStatusClass(instanceName, statusClass); 193 | } 194 | }; 195 | 196 | /** 197 | * Reset the squares. 198 | */ 199 | Squares.prototype.reset = function() { 200 | this.container_.empty(); 201 | this.squares_ = {}; 202 | }; 203 | 204 | /** 205 | * Colors the HTML element with the given color / class and jquery id. 206 | * @param {String} instanceName The name of the instance. 207 | * @param {String} color Class name to update. 208 | */ 209 | Squares.prototype.setStatusClass = function(instanceName, color) { 210 | square = this.squares_[instanceName]; 211 | if (square) { 212 | for (var status in this.statusClasses_) { 213 | square.removeClass(this.statusClasses_[status]); 214 | } 215 | square.addClass(color); 216 | } 217 | }; 218 | 219 | /** 220 | * Get the div for an instance. 221 | * @param {string} instanceName The instance. 222 | * @return {JQuery} A JQuery object wrapping the div that 223 | * represents instanceName. 224 | */ 225 | Squares.prototype.getSquareDiv = function(instanceName) { 226 | return this.squares_[instanceName]; 227 | }; 228 | -------------------------------------------------------------------------------- /demo-suite/demos/image-magick/main.py: -------------------------------------------------------------------------------- 1 | # Copyright 2012 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Image Magick demo.""" 16 | 17 | from __future__ import with_statement 18 | 19 | __author__ = 'kbrisbin@google.com (Kathryn Hurley)' 20 | 21 | import os 22 | import random 23 | 24 | import lib_path 25 | import google_cloud.gce as gce 26 | import google_cloud.gce_appengine as gce_appengine 27 | import google_cloud.gcs_appengine as gcs_appengine 28 | import google_cloud.oauth as oauth 29 | import jinja2 30 | import oauth2client.appengine as oauth2client 31 | import user_data 32 | import webapp2 33 | 34 | from google.appengine.api import users 35 | 36 | DEMO_NAME = 'image-magick' 37 | IMAGE = 'image-magick-demo-image' 38 | IMAGES = ['android', 'appengine', 'apps', 'chrome', 'games', 'gplus', 39 | 'maps', 'wallet', 'youtube'] 40 | SEQUENCES = ['5 5 360', '355 -5 0'] 41 | 42 | jinja_environment = jinja2.Environment(loader=jinja2.FileSystemLoader('')) 43 | oauth_decorator = oauth.decorator 44 | user_data.DEFAULTS[user_data.GCS_BUCKET]['label'] += (' (must have CORS and ' 45 | 'public-read ACLs set)') 46 | parameters = [ 47 | user_data.DEFAULTS[user_data.GCE_PROJECT_ID], 48 | user_data.DEFAULTS[user_data.GCS_PROJECT_ID], 49 | user_data.DEFAULTS[user_data.GCS_BUCKET], 50 | user_data.DEFAULTS[user_data.GCS_DIRECTORY], 51 | ] 52 | data_handler = user_data.DataHandler(DEMO_NAME, parameters) 53 | 54 | 55 | class ImageMagick(webapp2.RequestHandler): 56 | """Show main page for the Image Magick demo.""" 57 | 58 | @oauth_decorator.oauth_required 59 | @data_handler.data_required 60 | def get(self): 61 | """Display the main page for the Image Magick demo.""" 62 | 63 | if not oauth_decorator.credentials.refresh_token: 64 | self.redirect(oauth_decorator.authorize_url() + '&approval_prompt=force') 65 | 66 | gcs_bucket = data_handler.stored_user_data[user_data.GCS_BUCKET] 67 | gcs_directory = data_handler.stored_user_data.get( 68 | user_data.GCS_DIRECTORY, None) 69 | variables = { 70 | 'demo_name': DEMO_NAME, 71 | 'bucket': gcs_bucket, 72 | 'directory': gcs_directory, 73 | } 74 | template = jinja_environment.get_template( 75 | 'demos/%s/templates/index.html' % DEMO_NAME) 76 | self.response.out.write(template.render(variables)) 77 | 78 | 79 | class Instance(webapp2.RequestHandler): 80 | """Start and list instances.""" 81 | 82 | @oauth_decorator.oauth_required 83 | @data_handler.data_required 84 | def get(self): 85 | """Get and return the list of instances with names containing the tag.""" 86 | 87 | gce_project_id = data_handler.stored_user_data[user_data.GCE_PROJECT_ID] 88 | gce_project = gce.GceProject( 89 | oauth_decorator.credentials, project_id=gce_project_id) 90 | gce_appengine.GceAppEngine().list_demo_instances( 91 | self, gce_project, DEMO_NAME) 92 | 93 | @data_handler.data_required 94 | def post(self): 95 | """Insert instances with a startup script, metadata, and scopes. 96 | 97 | Startup script is randomly chosen to either rotate images left or right. 98 | Metadata includes the image to rotate, the demo name tag, and the machine 99 | number. Service account scopes include Compute and storage. 100 | """ 101 | 102 | user = users.get_current_user() 103 | credentials = oauth2client.StorageByKeyName( 104 | oauth2client.CredentialsModel, user.user_id(), 'credentials').get() 105 | gce_project_id = data_handler.stored_user_data[user_data.GCE_PROJECT_ID] 106 | gce_project = gce.GceProject(credentials, project_id=gce_project_id) 107 | 108 | # Get the bucket info for the instance metadata. 109 | gcs_bucket = data_handler.stored_user_data[user_data.GCS_BUCKET] 110 | gcs_directory = data_handler.stored_user_data.get( 111 | user_data.GCS_DIRECTORY, None) 112 | gcs_path = None 113 | if gcs_directory: 114 | gcs_path = '%s/%s' % (gcs_bucket, gcs_directory) 115 | else: 116 | gcs_path = gcs_bucket 117 | 118 | # Figure out the image. Use custom image if it exists. 119 | (image_project, image_name) = self._get_image_name(gce_project) 120 | 121 | # Create a list of instances to insert. 122 | instances = [] 123 | num_instances = int(self.request.get('num_instances')) 124 | network = gce.Network('default') 125 | network.gce_project = gce_project 126 | ext_net = [{'network': network.url, 127 | 'accessConfigs': [{'name': 'External IP access config', 128 | 'type': 'ONE_TO_ONE_NAT' 129 | }] 130 | }] 131 | for i in range(num_instances): 132 | startup_script = os.path.join(os.path.dirname(__file__), 'startup.sh') 133 | instance_name='%s-%d' % (DEMO_NAME, i) 134 | instances.append(gce.Instance( 135 | name=instance_name, 136 | network_interfaces=ext_net, 137 | service_accounts=gce_project.settings['cloud_service_account'], 138 | disk_mounts=[gce.DiskMount(boot=True, 139 | init_disk_name=instance_name, 140 | init_disk_image=image_name, 141 | init_disk_project=image_project, 142 | auto_delete=True)], 143 | metadata=[ 144 | {'key': 'startup-script', 'value': open( 145 | startup_script, 'r').read()}, 146 | {'key': 'image', 'value': random.choice(IMAGES)}, 147 | {'key': 'seq', 'value': random.choice(SEQUENCES)}, 148 | {'key': 'machine-num', 'value': i}, 149 | {'key': 'tag', 'value': DEMO_NAME}, 150 | {'key': 'gcs-path', 'value': gcs_path}])) 151 | 152 | response = gce_appengine.GceAppEngine().run_gce_request( 153 | self, 154 | gce_project.bulk_insert, 155 | 'Error inserting instances: ', 156 | resources=instances) 157 | 158 | if response: 159 | self.response.headers['Content-Type'] = 'text/plain' 160 | self.response.out.write('starting cluster') 161 | 162 | def _get_image_name(self, gce_project): 163 | """Finds the appropriate image to use. 164 | 165 | Args: 166 | gce_project: An instance of gce.GceProject 167 | 168 | Returns: 169 | A tuple containing the image project and image name. 170 | """ 171 | if gce_project.list_images(filter='name eq ' + IMAGE): 172 | return (gce_project.project_id, IMAGE) 173 | return (None, None) 174 | 175 | 176 | class GceCleanup(webapp2.RequestHandler): 177 | """Stop instances.""" 178 | 179 | @data_handler.data_required 180 | def post(self): 181 | """Stop instances with names containing the tag.""" 182 | 183 | user = users.get_current_user() 184 | credentials = oauth2client.StorageByKeyName( 185 | oauth2client.CredentialsModel, user.user_id(), 'credentials').get() 186 | gce_project_id = data_handler.stored_user_data[user_data.GCE_PROJECT_ID] 187 | gce_project = gce.GceProject(credentials, project_id=gce_project_id) 188 | gce_appengine.GceAppEngine().delete_demo_instances( 189 | self, gce_project, DEMO_NAME) 190 | 191 | 192 | class GcsCleanup(webapp2.RequestHandler): 193 | """Remove Cloud Storage files.""" 194 | 195 | @data_handler.data_required 196 | def post(self): 197 | """Remove all cloud storage contents from the given bucket and dir.""" 198 | 199 | user_id = users.get_current_user().user_id() 200 | credentials = oauth2client.StorageByKeyName( 201 | oauth2client.CredentialsModel, user_id, 'credentials').get() 202 | gcs_project_id = data_handler.stored_user_data[user_data.GCS_PROJECT_ID] 203 | gcs_bucket = data_handler.stored_user_data[user_data.GCS_BUCKET] 204 | gcs_directory = data_handler.stored_user_data.get( 205 | user_data.GCS_DIRECTORY, None) 206 | gcs_helper = gcs_appengine.GcsAppEngineHelper(credentials, gcs_project_id) 207 | file_regex = None 208 | if gcs_directory: 209 | file_regex = r'^%s/%s.*' % (gcs_directory, DEMO_NAME) 210 | else: 211 | file_regex = r'^%s.*' % DEMO_NAME 212 | gcs_helper.delete_bucket_contents( 213 | gcs_bucket, gcs_directory, file_regex) 214 | self.response.headers['Content-Type'] = 'text/plain' 215 | self.response.out.write('cleaning cloud storage bucket') 216 | 217 | 218 | app = webapp2.WSGIApplication( 219 | [ 220 | ('/%s' % DEMO_NAME, ImageMagick), 221 | ('/%s/instance' % DEMO_NAME, Instance), 222 | ('/%s/gce-cleanup' % DEMO_NAME, GceCleanup), 223 | ('/%s/gcs-cleanup' % DEMO_NAME, GcsCleanup), 224 | (data_handler.url_path, data_handler.data_handler), 225 | ], debug=True, config={'config': 'imagemagick'}) 226 | -------------------------------------------------------------------------------- /demo-suite/static/js/counter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This program is free software; you can redistribute it and/or 3 | * modify it under the terms of the GNU General Public License 4 | * as published by the Free Software Foundation; either version 2 5 | * of the License, or (at your option) any later version. 6 | * 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | * 12 | * You should have received a copy of the GNU General Public License 13 | * along with this program; if not, write to the Free Software 14 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 15 | * 16 | * @fileoverview Display a counter. 17 | * 18 | * Displays a counter representing the number of running instances. Originally 19 | * developed by Mark Crossley, revision 0.3.0. 20 | * http://www.wilmslowastro.com/odometer/odometer.html 21 | */ 22 | 23 | /** 24 | * Counter class displays a counter in the given HTML element. 25 | * @constructor 26 | * @param {Element} container The HTML element in which to display the 27 | * counter. 28 | * @param {string} targetState Either 'RUNNING' or 'TOTAL' to 29 | * differentiate which state class to count. 30 | * @param {Object} counterOptions Options for the counter object details can 31 | * be found here: http://www.wilmslowastro.com/odometer/odometer.html. 32 | */ 33 | var Counter = function(container, targetState, counterOptions) { 34 | if (!counterOptions) { 35 | counterOptions = { 36 | height: 30, 37 | digits: 4, 38 | decimals: 0, 39 | wobbleFactor: 0 40 | }; 41 | } 42 | var counterElement = container.getContext('2d'); 43 | this.counter_ = new odometer_(counterElement, counterOptions); 44 | 45 | this.targetState = 'RUNNING'; 46 | if (targetState) { 47 | this.targetState = targetState; 48 | } 49 | }; 50 | 51 | /** 52 | * The odometer (counter) object. 53 | * @type {Object} 54 | * @private 55 | */ 56 | Counter.prototype.counter_ = null; 57 | 58 | /** 59 | * Update the number of running instances. This method is called by the 60 | * Gce.heartbeat method, and is passed a dictionary containing the instance 61 | * status list and the num of running instances. 62 | * @param {Object} updateData Object mapping 'data' to the instance status 63 | * information. 64 | */ 65 | Counter.prototype.update = function(updateData) { 66 | this.counter_.setValue(updateData['stateCount'][this.targetState]); 67 | }; 68 | 69 | /** 70 | * The odometer class. The following code was developed by Mark Crossley. 71 | * @param {Element} ctx The canvas element. 72 | * @param {Object} parameters Optional parameters for the display. 73 | * @private 74 | * @this odometer 75 | */ 76 | var odometer_ = function(ctx, parameters) { 77 | parameters = parameters || {}; 78 | var height = 79 | (undefined === parameters.height ? 40 : parameters.height); 80 | var digits = (undefined === parameters.digits ? 6 : parameters.digits); 81 | var decimals = (undefined === parameters.decimals ? 1 : parameters.decimals); 82 | var decimalBackColor = (undefined === parameters.decimalBackColor ? 83 | '#F0F0F0' : parameters.decimalBackColor); 84 | var decimalForeColor = (undefined === parameters.decimalForeColor ? 85 | '#F01010' : parameters.decimalForeColor); 86 | var font = (undefined === parameters.font ? 'sans-serif' : parameters.font); 87 | var value = (undefined === parameters.value ? 0 : parameters.value); 88 | var valueBackColor = (undefined === parameters.valueBackColor ? 89 | '#050505' : parameters.valueBackColor); 90 | var valueForeColor = (undefined === parameters.valueForeColor ? 91 | '#F8F8F8' : parameters.valueForeColor); 92 | var wobbleFactor = (undefined === parameters.wobbleFactor ? 93 | 0.07 : parameters.wobbleFactor); 94 | 95 | var doc = document; 96 | var initialized = false; 97 | 98 | // Cannot display negative values yet 99 | if (value < 0) { 100 | value = 0; 101 | } 102 | 103 | var digitHeight = Math.floor(height * 0.85); 104 | var stdFont = '600 ' + digitHeight + 'px ' + font; 105 | 106 | var digitWidth = Math.floor(height * 0.68); 107 | var width = digitWidth * (digits + decimals); 108 | var columnHeight = digitHeight * 11; 109 | var verticalSpace = columnHeight / 12; 110 | var zeroOffset = verticalSpace * 0.85; 111 | 112 | var wobble = []; 113 | 114 | // Resize and clear the main context 115 | ctx.canvas.width = width; 116 | ctx.canvas.height = height; 117 | 118 | // Create buffers 119 | var backgroundBuffer = createBuffer(width, height); 120 | var backgroundContext = backgroundBuffer.getContext('2d'); 121 | 122 | var foregroundBuffer = createBuffer(width, height); 123 | var foregroundContext = foregroundBuffer.getContext('2d'); 124 | 125 | var digitBuffer = createBuffer(digitWidth, columnHeight * 1.1); 126 | var digitContext = digitBuffer.getContext('2d'); 127 | 128 | var decimalBuffer = createBuffer(digitWidth, columnHeight * 1.1); 129 | var decimalContext = decimalBuffer.getContext('2d'); 130 | 131 | 132 | function init() { 133 | 134 | initialized = true; 135 | 136 | // Create the foreground 137 | foregroundContext.rect(0, 0, width, height); 138 | gradHighlight = foregroundContext.createLinearGradient(0, 0, 0, height); 139 | gradHighlight.addColorStop(0, 'rgba(0, 0, 0, 1)'); 140 | gradHighlight.addColorStop(0.1, 'rgba(0, 0, 0, 0.4)'); 141 | gradHighlight.addColorStop(0.33, 'rgba(255, 255, 255, 0.45)'); 142 | gradHighlight.addColorStop(0.46, 'rgba(255, 255, 255, 0)'); 143 | gradHighlight.addColorStop(0.9, 'rgba(0, 0, 0, 0.4)'); 144 | gradHighlight.addColorStop(1, 'rgba(0, 0, 0, 1)'); 145 | foregroundContext.fillStyle = gradHighlight; 146 | foregroundContext.fill(); 147 | 148 | // Create a digit column 149 | // background 150 | digitContext.rect(0, 0, digitWidth, columnHeight * 1.1); 151 | digitContext.fillStyle = valueBackColor; 152 | digitContext.fill(); 153 | // edges 154 | digitContext.strokeStyle = '#f0f0f0'; 155 | digitContext.lineWidth = '1px'; //height * 0.1 + "px"; 156 | digitContext.moveTo(0, 0); 157 | digitContext.lineTo(0, columnHeight * 1.1); 158 | digitContext.stroke(); 159 | digitContext.strokeStyle = '#202020'; 160 | digitContext.moveTo(digitWidth, 0); 161 | digitContext.lineTo(digitWidth, columnHeight * 1.1); 162 | digitContext.stroke(); 163 | // numerals 164 | digitContext.textAlign = 'center'; 165 | digitContext.textBaseline = 'middle'; 166 | digitContext.font = stdFont; 167 | digitContext.fillStyle = valueForeColor; 168 | // put the digits 901234567890 vertically into the buffer 169 | for (var i = 9; i < 21; i++) { 170 | digitContext.fillText(i % 10, digitWidth * 0.5, 171 | verticalSpace * (i - 9) + verticalSpace / 2); 172 | } 173 | 174 | // Create a decimal column 175 | if (decimals > 0) { 176 | // background 177 | decimalContext.rect(0, 0, digitWidth, columnHeight * 1.1); 178 | decimalContext.fillStyle = decimalBackColor; 179 | decimalContext.fill(); 180 | // edges 181 | decimalContext.strokeStyle = '#f0f0f0'; 182 | decimalContext.lineWidth = '1px'; //height * 0.1 + "px"; 183 | decimalContext.moveTo(0, 0); 184 | decimalContext.lineTo(0, columnHeight * 1.1); 185 | decimalContext.stroke(); 186 | decimalContext.strokeStyle = '#202020'; 187 | decimalContext.moveTo(digitWidth, 0); 188 | decimalContext.lineTo(digitWidth, columnHeight * 1.1); 189 | decimalContext.stroke(); 190 | // numerals 191 | decimalContext.textAlign = 'center'; 192 | decimalContext.textBaseline = 'middle'; 193 | decimalContext.font = stdFont; 194 | decimalContext.fillStyle = decimalForeColor; 195 | // put the digits 901234567890 vertically into the buffer 196 | for (var i = 9; i < 21; i++) { 197 | decimalContext.fillText(i % 10, digitWidth * 0.5, 198 | verticalSpace * (i - 9) + verticalSpace / 2); 199 | } 200 | } 201 | // wobble factors 202 | for (var i = 0; i < (digits + decimals); i++) { 203 | wobble[i] = 204 | Math.random() * wobbleFactor * height - wobbleFactor * height / 2; 205 | } 206 | } 207 | 208 | function drawDigits() { 209 | var pos = 1; 210 | var val; 211 | 212 | val = value; 213 | // do not use Math.pow() - rounding errors! 214 | for (var i = 0; i < decimals; i++) { 215 | val *= 10; 216 | } 217 | 218 | var numb = Math.floor(val); 219 | var frac = val - numb; 220 | numb = String(numb); 221 | var prevNum = 9; 222 | 223 | for (var i = 0; i < decimals + digits; i++) { 224 | var num = +numb.substring(numb.length - i - 1, numb.length - i) || 0; 225 | if (prevNum != 9) { 226 | frac = 0; 227 | } 228 | if (i < decimals) { 229 | backgroundContext.drawImage(decimalBuffer, width - digitWidth * pos, 230 | -(verticalSpace * (num + frac) + zeroOffset + wobble[i])); 231 | } else { 232 | backgroundContext.drawImage(digitBuffer, width - digitWidth * pos, 233 | -(verticalSpace * (num + frac) + zeroOffset + wobble[i])); 234 | } 235 | pos++; 236 | prevNum = num; 237 | } 238 | } 239 | 240 | this.setValue = function(newVal) { 241 | value = newVal; 242 | if (value < 0) { 243 | value = 0; 244 | } 245 | this.repaint(); 246 | }; 247 | 248 | this.getValue = function() { 249 | return value; 250 | }; 251 | 252 | this.repaint = function() { 253 | if (!initialized) { 254 | init(); 255 | } 256 | 257 | // draw digits 258 | drawDigits(); 259 | 260 | // draw the foreground 261 | backgroundContext.drawImage(foregroundBuffer, 0, 0); 262 | 263 | // paint back to the main context 264 | ctx.drawImage(backgroundBuffer, 0, 0); 265 | }; 266 | 267 | this.repaint(); 268 | 269 | function createBuffer(width, height) { 270 | var buffer = doc.createElement('canvas'); 271 | buffer.width = width; 272 | buffer.height = height; 273 | return buffer; 274 | } 275 | }; 276 | -------------------------------------------------------------------------------- /demo-suite/demos/quick-start/main.py: -------------------------------------------------------------------------------- 1 | # Copyright 2012 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Main page for the Google Compute Engine demo suite.""" 16 | 17 | from __future__ import with_statement 18 | 19 | __author__ = 'kbrisbin@google.com (Kathryn Hurley)' 20 | 21 | import lib_path 22 | import logging 23 | import google_cloud.gce as gce 24 | import google_cloud.gce_appengine as gce_appengine 25 | import google_cloud.oauth as oauth 26 | import jinja2 27 | import oauth2client.appengine as oauth2client 28 | import time 29 | import user_data 30 | import webapp2 31 | 32 | from google.appengine.ext import ndb 33 | from google.appengine.api import users 34 | 35 | DEMO_NAME = 'quick-start' 36 | 37 | class Objective(ndb.Model): 38 | """ This data model keeps track of work in progress. """ 39 | # Disable caching of objective. 40 | _use_memcache = False 41 | _use_cache = False 42 | 43 | # Desired number of VMs and start time. This will be >0 for a start 44 | # request or 0 for a reset/stop request. 45 | targetVMs = ndb.IntegerProperty() 46 | 47 | # Number of VMs started by last start request. This is handy when 48 | # recovering during a reset operation, so we can figure out how many 49 | # instances to depict in the UI. 50 | startedVMs = ndb.IntegerProperty() 51 | 52 | # Epoch time when last/current request was stated. 53 | startTime = ndb.IntegerProperty() 54 | 55 | def getObjective(project_id): 56 | key = ndb.Key("Objective", project_id) 57 | return key.get() 58 | 59 | @ndb.transactional 60 | def updateObjective(project_id, targetVMs): 61 | objective = getObjective(project_id) 62 | if not objective: 63 | logging.info('objective not found, creating new, project=' + project_id) 64 | key = ndb.Key("Objective", project_id) 65 | objective = Objective(key=key) 66 | objective.targetVMs = targetVMs 67 | # Overwrite startedVMs only when starting, skip when stopping. 68 | if targetVMs > 0: 69 | objective.startedVMs = targetVMs 70 | objective.startTime = int(time.time()) 71 | objective.put() 72 | 73 | def getUserDemoInfo(user): 74 | try: 75 | ldap = user.nickname().split('@')[0] 76 | except: 77 | ldap = 'unknown' 78 | logging.info('User without a nickname') 79 | 80 | gce_id = data_handler.stored_user_data[user_data.GCE_PROJECT_ID] 81 | demo_id = '%s-%s' % (DEMO_NAME, ldap) 82 | project_id = '%s-%s' % (gce_id, ldap) 83 | 84 | return dict(demo_id=demo_id, ldap=ldap, project_id=project_id) 85 | 86 | jinja_environment = jinja2.Environment(loader=jinja2.FileSystemLoader('')) 87 | oauth_decorator = oauth.decorator 88 | parameters = [ 89 | user_data.DEFAULTS[user_data.GCE_PROJECT_ID], 90 | user_data.DEFAULTS[user_data.GCE_ZONE_NAME] 91 | ] 92 | data_handler = user_data.DataHandler(DEMO_NAME, parameters) 93 | 94 | 95 | class QuickStart(webapp2.RequestHandler): 96 | """Show main Quick Start demo page.""" 97 | 98 | @oauth_decorator.oauth_required 99 | @data_handler.data_required 100 | def get(self): 101 | """Displays the main page for the Quick Start demo. Auth required.""" 102 | user_info = getUserDemoInfo(users.get_current_user()) 103 | 104 | if not oauth_decorator.credentials.refresh_token: 105 | self.redirect(oauth_decorator.authorize_url() + '&approval_prompt=force') 106 | 107 | targetVMs = 5 108 | startedVMs = 5 109 | startTime = 0 110 | 111 | objective = getObjective(user_info['project_id']) 112 | if objective: 113 | (targetVMs, startedVMs, startTime) = (objective.targetVMs, 114 | objective.startedVMs, objective.startTime) 115 | 116 | variables = { 117 | 'demo_name': DEMO_NAME, 118 | 'demo_id': user_info['demo_id'], 119 | 'targetVMs': targetVMs, 120 | 'startedVMs': startedVMs, 121 | 'startTime': startTime, 122 | } 123 | template = jinja_environment.get_template( 124 | 'demos/%s/templates/index.html' % DEMO_NAME) 125 | self.response.out.write(template.render(variables)) 126 | 127 | 128 | class Instance(webapp2.RequestHandler): 129 | """List or start instances.""" 130 | 131 | @oauth_decorator.oauth_required 132 | @data_handler.data_required 133 | def get(self): 134 | """List instances using the gce_appengine helper class. 135 | 136 | Return the results as JSON mapping instance name to status. 137 | """ 138 | user_info = getUserDemoInfo(users.get_current_user()) 139 | 140 | gce_project_id = data_handler.stored_user_data[user_data.GCE_PROJECT_ID] 141 | gce_zone_name = data_handler.stored_user_data[user_data.GCE_ZONE_NAME] 142 | gce_project = gce.GceProject( 143 | oauth_decorator.credentials, project_id=gce_project_id, 144 | zone_name=gce_zone_name) 145 | gce_appengine.GceAppEngine().list_demo_instances( 146 | self, gce_project, user_info['demo_id']) 147 | 148 | @data_handler.data_required 149 | def post(self): 150 | """Start instances using the gce_appengine helper class.""" 151 | user_info = getUserDemoInfo(users.get_current_user()) 152 | 153 | gce_project_id = data_handler.stored_user_data[user_data.GCE_PROJECT_ID] 154 | gce_zone_name = data_handler.stored_user_data[user_data.GCE_ZONE_NAME] 155 | user_id = users.get_current_user().user_id() 156 | credentials = oauth2client.StorageByKeyName( 157 | oauth2client.CredentialsModel, user_id, 'credentials').get() 158 | gce_project = gce.GceProject(credentials, project_id=gce_project_id, 159 | zone_name=gce_zone_name) 160 | 161 | # Create a user specific route. We will apply this route to all 162 | # instances without an IP address so their requests are routed 163 | # through the first instance acting as a proxy. 164 | # gce_project.list_routes() 165 | proxy_instance = gce.Instance(name='%s-0' % user_info['demo_id'], 166 | zone_name=gce_zone_name) 167 | proxy_instance.gce_project = gce_project 168 | route_name = '%s-0' % user_info['demo_id'] 169 | gce_route = gce.Route(name=route_name, 170 | network_name='default', 171 | destination_range='0.0.0.0/0', 172 | next_hop_instance=proxy_instance, 173 | priority=200, 174 | tags=['qs-%s' % user_info['ldap']]) 175 | response = gce_appengine.GceAppEngine().run_gce_request( 176 | self, 177 | gce_project.insert, 178 | 'Error inserting route: ', 179 | resource=gce_route) 180 | 181 | # Define a network interfaces list here that requests an ephemeral 182 | # external IP address. We will apply this configuration to the first 183 | # VM started by quick start. All other VMs will take the default 184 | # network configuration, which requests no external IP address. 185 | network = gce.Network('default') 186 | network.gce_project = gce_project 187 | ext_net = [{ 'network': network.url, 188 | 'accessConfigs': [{ 'name': 'External IP access config', 189 | 'type': 'ONE_TO_ONE_NAT' 190 | }] 191 | }] 192 | num_instances = int(self.request.get('num_instances')) 193 | instances = [ gce.Instance('%s-%d' % (user_info['demo_id'], i), 194 | zone_name=gce_zone_name, 195 | network_interfaces=(ext_net if i == 0 else None), 196 | metadata=([{ 197 | 'key': 'startup-script', 198 | 'value': user_data.STARTUP_SCRIPT % 'false' 199 | }] if i==0 else [{ 200 | 'key': 'startup-script', 201 | 'value': user_data.STARTUP_SCRIPT % 'true' 202 | }]), 203 | service_accounts=[{'email': 'default', 204 | 'scopes': ['https://www.googleapis.com/auth/compute']}], 205 | disk_mounts=[gce.DiskMount( 206 | init_disk_name='%s-%d' % (user_info['demo_id'], i), boot=True)], 207 | can_ip_forward=(True if i == 0 else False), 208 | tags=(['qs-proxy'] if i == 0 else ['qs-%s' % user_info['ldap']])) 209 | for i in range(num_instances) ] 210 | response = gce_appengine.GceAppEngine().run_gce_request( 211 | self, 212 | gce_project.bulk_insert, 213 | 'Error inserting instances: ', 214 | resources=instances) 215 | 216 | # Record objective in datastore so we can recover work in progress. 217 | updateObjective(user_info['project_id'], num_instances) 218 | 219 | if response: 220 | self.response.headers['Content-Type'] = 'text/plain' 221 | self.response.out.write('starting cluster') 222 | 223 | 224 | class Cleanup(webapp2.RequestHandler): 225 | """Stop instances.""" 226 | 227 | @data_handler.data_required 228 | def post(self): 229 | """Stop instances using the gce_appengine helper class.""" 230 | user_info = getUserDemoInfo(users.get_current_user()) 231 | gce_project_id = data_handler.stored_user_data[user_data.GCE_PROJECT_ID] 232 | gce_zone_name = data_handler.stored_user_data[user_data.GCE_ZONE_NAME] 233 | user_id = users.get_current_user().user_id() 234 | credentials = oauth2client.StorageByKeyName( 235 | oauth2client.CredentialsModel, user_id, 'credentials').get() 236 | gce_project = gce.GceProject(credentials, project_id=gce_project_id, 237 | zone_name=gce_zone_name) 238 | gce_appengine.GceAppEngine().delete_demo_instances( 239 | self, gce_project, user_info['demo_id']) 240 | 241 | # Record reset objective in datastore so we can recover work in progress. 242 | updateObjective(user_info['project_id'], 0) 243 | 244 | gce_appengine.GceAppEngine().delete_demo_route( 245 | self, gce_project, '%s-0' % user_info['demo_id']) 246 | 247 | app = webapp2.WSGIApplication( 248 | [ 249 | ('/%s' % DEMO_NAME, QuickStart), 250 | ('/%s/instance' % DEMO_NAME, Instance), 251 | ('/%s/cleanup' % DEMO_NAME, Cleanup), 252 | (data_handler.url_path, data_handler.data_handler), 253 | ], 254 | debug=True) 255 | -------------------------------------------------------------------------------- /demo-suite/static/js/gce.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2012 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | s WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @fileoverview GCE JavaScript functions. 17 | * 18 | * Start, stop, or cleanup instances. Set UI controls. 19 | * 20 | */ 21 | 22 | /** 23 | * Gce class starts, stops instances and controls UI. 24 | * @constructor 25 | * @param {string} startInstanceUrl The URL to start instances. 26 | * @param {string} listInstanceUrl The URL to list instances. 27 | * @param {string} stopInstanceUrl The URL to stop instances. 28 | * @param {Object} gceUiOptions UI options for GCE. 29 | * @param {Object} commonQueryData Common parameters to pass with each request. 30 | */ 31 | var Gce = function(startInstanceUrl, listInstanceUrl, stopInstanceUrl, 32 | gceUiOptions, commonQueryData) { 33 | 34 | /** 35 | * The URL to start instances. 36 | * @type {string} 37 | * @private 38 | */ 39 | this.startInstanceUrl_ = startInstanceUrl; 40 | 41 | /** 42 | * The URL to list instances. 43 | * @type {string} 44 | * @private 45 | */ 46 | this.listInstanceUrl_ = listInstanceUrl; 47 | 48 | /** 49 | * The URL to stop instances. 50 | * @type {string} 51 | * @private 52 | */ 53 | this.stopInstanceUrl_ = stopInstanceUrl; 54 | 55 | /** 56 | * Object mapping Ajax response code to handler. 57 | * @type {Object} 58 | * @private 59 | */ 60 | this.statusCodeResponseFunctions_ = { 61 | 401: function(jqXHR, textStatus, errorThrown) { 62 | alert('Refresh token revoked! ' + textStatus + ':' + errorThrown); 63 | }, 64 | 500: function(jqXHR, textStatus, errorThrown) { 65 | alert('Unknown error! ' + textStatus + ':' + errorThrown); 66 | } 67 | }; 68 | 69 | /** 70 | * Query data to be passed with every request. 71 | * @type {Object} 72 | * @private 73 | */ 74 | this.commonQueryData_ = commonQueryData; 75 | 76 | /** 77 | * Are we doing continuous heartbeats to the server? If yes, we don't do 78 | * heartbeats specifically for start/stop operations and so never generate UI 79 | * start/stop messages. 80 | * @type {Boolean} 81 | * @private 82 | */ 83 | this.doContinuousHeartbeat_ = false; 84 | 85 | this.setOptions(gceUiOptions); 86 | }; 87 | 88 | 89 | 90 | /** 91 | * Time (ms) between calls to server to check for running instances. 92 | * @type {number} 93 | * @private 94 | */ 95 | Gce.prototype.HEARTBEAT_TIMEOUT_ = 2000; 96 | 97 | 98 | /** 99 | * The various states (status in the GCE API) that an instance can be in. The 100 | * UNKNOWN and SERVING states are synthetic. 101 | * @type {Array} 102 | * @private 103 | */ 104 | Gce.prototype.STATES = [ 105 | 'UNKNOWN', 106 | 'PROVISIONING', 107 | 'STAGING', 108 | 'RUNNING', 109 | 'SERVING', 110 | 'STOPPING', 111 | 'STOPPED', 112 | 'TERMINATED', 113 | ]; 114 | 115 | /** 116 | * Sets the GCE UI options. Options include colored squares to indicate 117 | * status, timer, and counter. 118 | * @param {Object} gceUiOptions UI options for demos. 119 | */ 120 | Gce.prototype.setOptions = function(gceUiOptions) { 121 | this.gceUiOptions = gceUiOptions; 122 | }; 123 | 124 | /** 125 | * Send the Ajax request to start instances. Init UI controls with start 126 | * method. 127 | * @param {number} numInstances The number of instances to start. 128 | * @param {Object} startOptions Consists of startOptions.data and 129 | * startOptions.callback. 130 | */ 131 | Gce.prototype.startInstances = function(numInstances, startOptions) { 132 | for (var gceUi in this.gceUiOptions) { 133 | if (this.gceUiOptions[gceUi].start) { 134 | this.gceUiOptions[gceUi].start(); 135 | } 136 | } 137 | 138 | if ((typeof Recovering === 'undefined') || (!Recovering)) { 139 | var ajaxRequest = { 140 | type: 'POST', 141 | url: this.startInstanceUrl_, 142 | dataType: 'json', 143 | statusCode: this.statusCodeResponseFunctions_, 144 | complete: startOptions.ajaxComplete, 145 | }; 146 | ajaxRequest.data = {} 147 | if (startOptions.data) { 148 | ajaxRequest.data = startOptions.data; 149 | } 150 | if (this.commonQueryData_) { 151 | $.extend(ajaxRequest.data, this.commonQueryData_) 152 | } 153 | $.ajax(ajaxRequest); 154 | } 155 | if (!this.doContinuousHeartbeat_ 156 | && (this.gceUiOptions || startOptions.callback)) { 157 | var terminalState = 'RUNNING' 158 | if (startOptions.checkServing) { 159 | terminalState = 'SERVING' 160 | } 161 | this.heartbeat_(numInstances, startOptions.callback, terminalState); 162 | } 163 | }; 164 | 165 | /** 166 | * Send the Ajax request to stop instances. 167 | * @param {function} callback A callback function to call when instances 168 | * have stopped. 169 | */ 170 | Gce.prototype.stopInstances = function(callback) { 171 | var data = {} 172 | 173 | if (this.gceUiOptions.timer && this.gceUiOptions.timer.start) { 174 | this.gceUiOptions.timer.start(); 175 | } 176 | 177 | if (this.commonQueryData_) { 178 | $.extend(data, this.commonQueryData_) 179 | } 180 | 181 | $.ajax({ 182 | type: 'POST', 183 | url: this.stopInstanceUrl_, 184 | statusCode: this.statusCodeResponseFunctions_, 185 | data: data 186 | }); 187 | if (!this.doContinuousHeartbeat_ 188 | && (this.gceUiOptions || startOptions.callback)) { 189 | this.heartbeat_(0, callback, 'TOTAL'); 190 | } 191 | }; 192 | 193 | 194 | /** 195 | * Get an update on instance states and status. 196 | * @param {function} callback A function to call when AJAX request completes. 197 | * @param {Object} optionalData Optional data to send with the request. 198 | */ 199 | Gce.prototype.getInstanceStates = function(callback, optionalData) { 200 | var that = this; 201 | var processResults = function(data) { 202 | callback(data); 203 | } 204 | this.getStatuses_(processResults, optionalData) 205 | }; 206 | 207 | /** 208 | * Start continuous heartbeat. If this is activated we no longer do heartbeats 209 | * specifically for start/stopInstances and UI components no longer get 210 | * corresponding start/stop calls. 211 | * @param {Function} callback A callback invoked on each heartbeat 212 | */ 213 | Gce.prototype.startContinuousHeartbeat = function(callback) { 214 | if (!this.doContinuousHeartbeat_) { 215 | this.doContinuousHeartbeat_ = true; 216 | this.continuousHeartbeat_(callback) 217 | } 218 | }; 219 | 220 | /** 221 | * Send UI update messages when we get data on what is running and how. 222 | * @param {Object} data Data returned from GCE API with summary data. 223 | * @private 224 | */ 225 | Gce.prototype.updateUI_ = function(data) { 226 | for (var gceUi in this.gceUiOptions) { 227 | if (this.gceUiOptions[gceUi].update) { 228 | this.gceUiOptions[gceUi].update(data); 229 | } 230 | } 231 | }; 232 | 233 | /** 234 | * Send the Ajax request to start instances. Update UI controls with an update 235 | * method. 236 | * @param {number} numInstances The number of instances that are starting. 237 | * @param {function} callback A callback function to call when instances have 238 | * started or stopped. 239 | * @param {string} terminalState Stop the heartbeat when all numInstances are 240 | * in this state. 241 | * @private 242 | */ 243 | Gce.prototype.heartbeat_ = function(numInstances, callback, terminalState) { 244 | var that = this; 245 | var success = function(data) { 246 | isDone = data['stateCount'][terminalState] == numInstances; 247 | 248 | if (isDone) { 249 | for (var gceUi in that.gceUiOptions) { 250 | if (that.gceUiOptions[gceUi].stop) { 251 | that.gceUiOptions[gceUi].stop(); 252 | } 253 | } 254 | 255 | if (callback) { 256 | callback(data); 257 | } 258 | } else { 259 | that.heartbeat_(numInstances, callback, terminalState); 260 | } 261 | }; 262 | 263 | // If we're in recovery mode (i.e. user refresh the page before request 264 | // is complete), start the polling immediately to refresh state ASAP, 265 | // instead of waiting 2s to refresh the display. 266 | var that = this; 267 | if ((typeof Recovering !== 'undefined') && Recovering) { 268 | that.getStatuses_(success); 269 | } else { 270 | setTimeout(function() { 271 | that.getStatuses_(success); 272 | }, this.HEARTBEAT_TIMEOUT_); 273 | } 274 | }; 275 | 276 | Gce.prototype.continuousHeartbeat_ = function(callback) { 277 | var that = this; 278 | var success = function(data) { 279 | if (callback) { 280 | callback(data); 281 | } 282 | that.continuousHeartbeat_(callback); 283 | }; 284 | 285 | var that = this; 286 | setTimeout(function() { 287 | that.getStatuses_(success); 288 | }, this.HEARTBEAT_TIMEOUT_); 289 | 290 | } 291 | 292 | /** 293 | * Send Ajax request to get instance information. 294 | * @param {function} success Function to call if request is successful. 295 | * @param {Object} optionalData Optional data to send with the request. The 296 | * data is added as URL parameters. 297 | * @private 298 | */ 299 | Gce.prototype.getStatuses_ = function(success, optionalData) { 300 | var that = this; 301 | var localSuccess = function(data) { 302 | that.summarizeStates(data); 303 | that.updateUI_(data); 304 | success(data); 305 | } 306 | 307 | var ajaxRequest = { 308 | type: 'GET', 309 | url: this.listInstanceUrl_, 310 | dataType: 'json', 311 | success: localSuccess, 312 | statusCode: this.statusCodeResponseFunctions_ 313 | }; 314 | ajaxRequest.data = {} 315 | if (optionalData) { 316 | ajaxRequest.data = optionalData; 317 | } 318 | if (this.commonQueryData_) { 319 | $.extend(ajaxRequest.data, this.commonQueryData_) 320 | } 321 | $.ajax(ajaxRequest); 322 | }; 323 | 324 | /** 325 | * Builds a histogram of how many instances are in what state. It writes it 326 | * into data as an item called stateCount 327 | * @param {Object} data Data returned from the GCE API formatted into a 328 | * dictionary. 329 | * @return {Object} A map from state to count. 330 | */ 331 | Gce.prototype.summarizeStates = function(data) { 332 | var states = {}; 333 | $.each(this.STATES, function(index, value) { 334 | states[value] = 0; 335 | }); 336 | states['TOTAL'] = 0; 337 | 338 | $.each(data['instances'], function(i, d) { 339 | state = d['status']; 340 | if (!states.hasOwnProperty(state)) { 341 | state = 'UNKNOWN'; 342 | } 343 | states[state]++; 344 | states['TOTAL']++; 345 | }); 346 | 347 | data['stateCount'] = states 348 | }; 349 | -------------------------------------------------------------------------------- /demo-suite/lib/user_data.py: -------------------------------------------------------------------------------- 1 | # Copyright 2012 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Models and handlers to store users' information in the datastore.""" 16 | 17 | __author__ = 'kbrisbin@google.com (Kathryn Hurley)' 18 | 19 | import json 20 | import logging 21 | import threading 22 | 23 | import jinja2 24 | import webapp2 25 | 26 | from google.appengine.api import users 27 | from google.appengine.ext import db 28 | 29 | jinja_environment = jinja2.Environment(loader=jinja2.FileSystemLoader('')) 30 | 31 | GCE_PROJECT_ID = 'gce-project-id' 32 | GCE_ZONE_NAME = 'gce-zone-name' 33 | GCE_LOAD_BALANCER_IP = 'gce-load-balancer-ip' 34 | GCS_PROJECT_ID = 'gcs-project-id' 35 | GCS_BUCKET = 'gcs-bucket' 36 | GCS_DIRECTORY = 'gcs-directory' 37 | DEFAULTS = { 38 | GCE_PROJECT_ID: { 39 | 'type': 'string', 40 | 'required': True, 41 | 'label': 'Compute Engine Project ID (e.g.: compute-engine-project)', 42 | 'name': GCE_PROJECT_ID 43 | }, 44 | GCE_ZONE_NAME: { 45 | 'type': 'string', 46 | 'required': True, 47 | 'label': 'Compute Engine Zone (e.g.: us-central2-a)', 48 | 'name': GCE_ZONE_NAME 49 | }, 50 | GCE_LOAD_BALANCER_IP: { 51 | 'type': 'list', 52 | 'required': False, 53 | 'label': 'Compute Engine Load Balancer public IP address(s) (e.g.: 1.2.3.4,2.2.2.2)', 54 | 'name': 'gce-load-balancer-ip' 55 | }, 56 | GCS_PROJECT_ID: { 57 | 'type': 'string', 58 | 'required': True, 59 | 'label': ('Cloud Storage Project ID (e.g.: 123456. Must be the same ' 60 | 'project as the Compute Engine project)'), 61 | 'name': GCS_PROJECT_ID 62 | }, 63 | GCS_BUCKET: { 64 | 'type': 'string', 65 | 'required': True, 66 | 'label': 'Cloud Storage Bucket Name', 67 | 'name': GCS_BUCKET 68 | }, 69 | GCS_DIRECTORY: { 70 | 'type': 'string', 71 | 'required': False, 72 | 'label': 'Cloud Storage Directory', 73 | 'name': GCS_DIRECTORY 74 | } 75 | } 76 | 77 | URL_PATH = '/%s/project' 78 | 79 | STARTUP_SCRIPT = ''' 80 | #!/bin/bash 81 | 82 | no_ip=%s 83 | 84 | if $no_ip; then 85 | sleep_time=10m 86 | else 87 | sleep_time=25m 88 | sudo sh -c "echo 1 > /proc/sys/net/ipv4/ip_forward" 89 | sudo iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE 90 | fi 91 | 92 | while sleep $sleep_time 93 | do 94 | if ! $no_ip; then 95 | gcutil deleteroute `hostname` --force 96 | fi 97 | 98 | gcutil deleteinstance `hostname` --force --delete_boot_pd 99 | done 100 | ''' 101 | 102 | class JsonProperty(db.Property): 103 | """JSON data stored in database. 104 | 105 | From - http://snipplr.com/view.php?codeview&id=10529 106 | """ 107 | 108 | data_type = db.TextProperty 109 | 110 | def get_value_for_datastore(self, model_instance): 111 | """Get the value to save in the data store. 112 | 113 | Args: 114 | model_instance: An dictionary instance of the model. 115 | 116 | Returns: 117 | The string representation of the database value. 118 | """ 119 | value = super(JsonProperty, self).get_value_for_datastore(model_instance) 120 | return self._deflate(value) 121 | 122 | def validate(self, value): 123 | """Validate the value. 124 | 125 | Args: 126 | value: The value to validate. 127 | 128 | Returns: 129 | The dictionary (JSON object). 130 | """ 131 | return self._inflate(value) 132 | 133 | def make_value_from_datastore(self, value): 134 | """Create a JSON object from the value in the datastore. 135 | 136 | Args: 137 | value: The string value in the datastore. 138 | 139 | Returns: 140 | The dictionary (JSON object). 141 | """ 142 | return self._inflate(value) 143 | 144 | def _inflate(self, value): 145 | """Convert the value to a dictionary. 146 | 147 | Args: 148 | value: The string value to convert to a dictionary. 149 | 150 | Returns: 151 | The dictionary (JSON object). 152 | """ 153 | if value is None: 154 | return {} 155 | if isinstance(value, unicode) or isinstance(value, str): 156 | return json.loads(value) 157 | return value 158 | 159 | def _deflate(self, value): 160 | """Convert the dictionary to string. 161 | 162 | Args: 163 | value: A dictionary. 164 | 165 | Returns: 166 | The string representation of the dictionary. 167 | """ 168 | return json.dumps(value) 169 | 170 | 171 | class UserData(db.Model): 172 | """Store the user data.""" 173 | user = db.UserProperty(required=True) 174 | user_data = JsonProperty() 175 | 176 | 177 | class DataHandler(object): 178 | """Store user data in database.""" 179 | 180 | def set_stored_user_data(self, stored_user_data): 181 | self._tls.stored_user_data = stored_user_data 182 | 183 | def get_stored_user_data(self): 184 | return self._tls.stored_user_data 185 | 186 | stored_user_data = property(get_stored_user_data, set_stored_user_data) 187 | 188 | def __init__(self, demo_name, parameters, redirect_uri=None): 189 | """Initializes the DataHandler class. 190 | 191 | An example of default parameters can be seen above. Each parameter is 192 | a dictionary. Fields include: type (the data type - not currently used, 193 | but will be useful for validation later), required (whether or not the 194 | data is required), label (a label for the HTML form), and name (the 195 | key for the JSON object in the database, and the name attribute in the 196 | HTML form). 197 | 198 | Demos can create additional parameters as needed. The data will be stored 199 | in the database indexed by user. All data is available to any other demo. 200 | 201 | Args: 202 | demo_name: The string name of the demo. 203 | parameters: A list of dictionaries specifying what data to store in the 204 | database for the current user. 205 | redirect_uri: The string URL to redirect to after a successful POST 206 | to store data in the database. Defaults to / if None. 207 | """ 208 | self._tls = threading.local() 209 | self._demo_name = demo_name 210 | self._parameters = parameters 211 | if redirect_uri: 212 | self._redirect_uri = redirect_uri 213 | else: 214 | self._redirect_uri = '/' + self._demo_name 215 | self.stored_user_data = {} 216 | 217 | @property 218 | def url_path(self): 219 | """The path for the User Data handler. 220 | 221 | Formatted as //project. 222 | 223 | Returns: 224 | The path as a string. 225 | """ 226 | return URL_PATH % self._demo_name 227 | 228 | def data_required(self, method): 229 | """Decorator to check if required user information is available. 230 | 231 | Redirects to form if info is not available. 232 | 233 | Args: 234 | method: callable function. 235 | 236 | Returns: 237 | Callable decorator function. 238 | """ 239 | 240 | def check_data(request_handler, *args, **kwargs): 241 | """Checks for required data and redirects to form if not present.. 242 | 243 | Args: 244 | request_handler: The app engine request handler method. 245 | *args: Any request arguments. 246 | **kwargs: Any request parameters. 247 | 248 | Returns: 249 | Callable function. 250 | """ 251 | user = users.get_current_user() 252 | if not user: 253 | return webapp2.redirect( 254 | users.create_login_url(request_handler.request.uri)) 255 | 256 | user_data = UserData.all().filter('user =', user).get() 257 | if user_data: 258 | self.stored_user_data = user_data.user_data 259 | 260 | for parameter in self._parameters: 261 | if parameter['required']: 262 | if not (user_data and user_data.user_data.get(parameter['name'])): 263 | return webapp2.redirect(self.url_path) 264 | 265 | try: 266 | return method(request_handler, *args, **kwargs) 267 | finally: 268 | self.stored_user_data = {} 269 | 270 | return check_data 271 | 272 | def data_handler(self, request): 273 | """Store user project information in the database. 274 | 275 | Args: 276 | request: The HTTP request. 277 | 278 | Returns: 279 | The webapp2.Response object. 280 | """ 281 | 282 | response = webapp2.Response() 283 | 284 | user = users.get_current_user() 285 | if user: 286 | if request.method == 'POST': 287 | return self._handle_post(request, user) 288 | 289 | elif request.method == 'GET': 290 | response = self._handle_get(response, user) 291 | 292 | else: 293 | response.set_status(405) 294 | response.headers['Content-Type'] = 'application/json' 295 | response.out.write({'error': 'Method not allowed.'}) 296 | 297 | else: 298 | response.set_status(401) 299 | response.headers['Content-Type'] = 'application/json' 300 | response.write({'error': 'User not logged in.'}) 301 | 302 | return response 303 | 304 | def _handle_get(self, response, user): 305 | """Handles GET requests and displays a form. 306 | 307 | Args: 308 | response: A webapp2.Response object. 309 | user: The current user. 310 | 311 | Returns: 312 | The modified webapp2.Response object. 313 | """ 314 | 315 | user_data = UserData.all().filter('user =', user).get() 316 | 317 | variables = {'demo_name': self._demo_name} 318 | variables['user_entered'] = {} 319 | if user_data: 320 | data = user_data.user_data 321 | # Convert 'list' typed user-data to a comma separated string 322 | # for easier user editing e.g. a,b vs. ['a', 'b']. 323 | for parameter in self._parameters: 324 | name = parameter['name'] 325 | if parameter['type'] == 'list': 326 | if name in data: 327 | data[name] = ','.join(data[name]) 328 | # Copy all saved values into the output. 329 | for name in user_data.user_data: 330 | variables['user_entered'][name] = data[name] 331 | 332 | variables['parameters'] = self._parameters 333 | 334 | template = jinja_environment.get_template('templates/project.html') 335 | response.out.write(template.render(variables)) 336 | return response 337 | 338 | def _handle_post(self, request, user): 339 | """Handles POST requests from the project form. 340 | 341 | Args: 342 | request: The HTTP request. 343 | user: The current user. 344 | 345 | Returns: 346 | A redirect to the redirect URI. 347 | """ 348 | 349 | user_data = UserData.all().filter('user =', user).get() 350 | new_user_data = {} 351 | if user_data: 352 | new_user_data = user_data.user_data 353 | 354 | for data in self._parameters: 355 | entered_value = request.get(data['name']) 356 | if not entered_value and data['required']: 357 | webapp2.redirect(URL_PATH) 358 | if data['type'] == 'list': 359 | # Convert string to list by spliting on commas and stripping whitespace. 360 | entered_value = [v.strip() for v in entered_value.split(',')] 361 | new_user_data[data['name']] = entered_value 362 | 363 | if user_data: 364 | user_data.user_data = new_user_data 365 | user_data.save() 366 | else: 367 | user_data = UserData(user=user, user_data=new_user_data) 368 | user_data.put() 369 | 370 | return webapp2.redirect(self._redirect_uri) 371 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /demo-suite/static/fontawesome/css/font-awesome.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 3.0.2 3 | * the iconic font designed for use with Twitter Bootstrap 4 | * ------------------------------------------------------- 5 | * The full suite of pictographic icons, examples, and documentation 6 | * can be found at: http://fortawesome.github.com/Font-Awesome/ 7 | * 8 | * License 9 | * ------------------------------------------------------- 10 | * - The Font Awesome font is licensed under the SIL Open Font License - http://scripts.sil.org/OFL 11 | * - Font Awesome CSS, LESS, and SASS files are licensed under the MIT License - 12 | * http://opensource.org/licenses/mit-license.html 13 | * - The Font Awesome pictograms are licensed under the CC BY 3.0 License - http://creativecommons.org/licenses/by/3.0/ 14 | * - Attribution is no longer required in Font Awesome 3.0, but much appreciated: 15 | * "Font Awesome by Dave Gandy - http://fortawesome.github.com/Font-Awesome" 16 | 17 | * Contact 18 | * ------------------------------------------------------- 19 | * Email: dave@davegandy.com 20 | * Twitter: http://twitter.com/fortaweso_me 21 | * Work: Lead Product Designer @ http://kyruus.com 22 | */ 23 | 24 | @font-face{ 25 | font-family:'FontAwesome'; 26 | src:url('../font/fontawesome-webfont.eot?v=3.0.1'); 27 | src:url('../font/fontawesome-webfont.eot?#iefix&v=3.0.1') format('embedded-opentype'), 28 | url('../font/fontawesome-webfont.woff?v=3.0.1') format('woff'), 29 | url('../font/fontawesome-webfont.ttf?v=3.0.1') format('truetype'); 30 | font-weight:normal; 31 | font-style:normal } 32 | 33 | [class^="icon-"],[class*=" icon-"]{font-family:FontAwesome;font-weight:normal;font-style:normal;text-decoration:inherit;-webkit-font-smoothing:antialiased;display:inline;width:auto;height:auto;line-height:normal;vertical-align:baseline;background-image:none;background-position:0 0;background-repeat:repeat;margin-top:0}.icon-white,.nav-pills>.active>a>[class^="icon-"],.nav-pills>.active>a>[class*=" icon-"],.nav-list>.active>a>[class^="icon-"],.nav-list>.active>a>[class*=" icon-"],.navbar-inverse .nav>.active>a>[class^="icon-"],.navbar-inverse .nav>.active>a>[class*=" icon-"],.dropdown-menu>li>a:hover>[class^="icon-"],.dropdown-menu>li>a:hover>[class*=" icon-"],.dropdown-menu>.active>a>[class^="icon-"],.dropdown-menu>.active>a>[class*=" icon-"],.dropdown-submenu:hover>a>[class^="icon-"],.dropdown-submenu:hover>a>[class*=" icon-"]{background-image:none}[class^="icon-"]:before,[class*=" icon-"]:before{text-decoration:inherit;display:inline-block;speak:none}a [class^="icon-"],a [class*=" icon-"]{display:inline-block}.icon-large:before{vertical-align:-10%;font-size:1.3333333333333333em}.btn [class^="icon-"],.nav [class^="icon-"],.btn [class*=" icon-"],.nav [class*=" icon-"]{display:inline}.btn [class^="icon-"].icon-large,.nav [class^="icon-"].icon-large,.btn [class*=" icon-"].icon-large,.nav [class*=" icon-"].icon-large{line-height:.9em}.btn [class^="icon-"].icon-spin,.nav [class^="icon-"].icon-spin,.btn [class*=" icon-"].icon-spin,.nav [class*=" icon-"].icon-spin{display:inline-block}.nav-tabs [class^="icon-"],.nav-pills [class^="icon-"],.nav-tabs [class*=" icon-"],.nav-pills [class*=" icon-"],.nav-tabs [class^="icon-"].icon-large,.nav-pills [class^="icon-"].icon-large,.nav-tabs [class*=" icon-"].icon-large,.nav-pills [class*=" icon-"].icon-large{line-height:.9em}li [class^="icon-"],.nav li [class^="icon-"],li [class*=" icon-"],.nav li [class*=" icon-"]{display:inline-block;width:1.25em;text-align:center}li [class^="icon-"].icon-large,.nav li [class^="icon-"].icon-large,li [class*=" icon-"].icon-large,.nav li [class*=" icon-"].icon-large{width:1.5625em}ul.icons{list-style-type:none;text-indent:-0.75em}ul.icons li [class^="icon-"],ul.icons li [class*=" icon-"]{width:.75em}.icon-muted{color:#eee}.icon-border{border:solid 1px #eee;padding:.2em .25em .15em;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.icon-2x{font-size:2em}.icon-2x.icon-border{border-width:2px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.icon-3x{font-size:3em}.icon-3x.icon-border{border-width:3px;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px}.icon-4x{font-size:4em}.icon-4x.icon-border{border-width:4px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px}.pull-right{float:right}.pull-left{float:left}[class^="icon-"].pull-left,[class*=" icon-"].pull-left{margin-right:.3em}[class^="icon-"].pull-right,[class*=" icon-"].pull-right{margin-left:.3em}.btn [class^="icon-"].pull-left.icon-2x,.btn [class*=" icon-"].pull-left.icon-2x,.btn [class^="icon-"].pull-right.icon-2x,.btn [class*=" icon-"].pull-right.icon-2x{margin-top:.18em}.btn [class^="icon-"].icon-spin.icon-large,.btn [class*=" icon-"].icon-spin.icon-large{line-height:.8em}.btn.btn-small [class^="icon-"].pull-left.icon-2x,.btn.btn-small [class*=" icon-"].pull-left.icon-2x,.btn.btn-small [class^="icon-"].pull-right.icon-2x,.btn.btn-small [class*=" icon-"].pull-right.icon-2x{margin-top:.25em}.btn.btn-large [class^="icon-"],.btn.btn-large [class*=" icon-"]{margin-top:0}.btn.btn-large [class^="icon-"].pull-left.icon-2x,.btn.btn-large [class*=" icon-"].pull-left.icon-2x,.btn.btn-large [class^="icon-"].pull-right.icon-2x,.btn.btn-large [class*=" icon-"].pull-right.icon-2x{margin-top:.05em}.btn.btn-large [class^="icon-"].pull-left.icon-2x,.btn.btn-large [class*=" icon-"].pull-left.icon-2x{margin-right:.2em}.btn.btn-large [class^="icon-"].pull-right.icon-2x,.btn.btn-large [class*=" icon-"].pull-right.icon-2x{margin-left:.2em}.icon-spin{display:inline-block;-moz-animation:spin 2s infinite linear;-o-animation:spin 2s infinite linear;-webkit-animation:spin 2s infinite linear;animation:spin 2s infinite linear}@-moz-keyframes spin{0%{-moz-transform:rotate(0deg)}100%{-moz-transform:rotate(359deg)}}@-webkit-keyframes spin{0%{-webkit-transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg)}}@-o-keyframes spin{0%{-o-transform:rotate(0deg)}100%{-o-transform:rotate(359deg)}}@-ms-keyframes spin{0%{-ms-transform:rotate(0deg)}100%{-ms-transform:rotate(359deg)}}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}}@-moz-document url-prefix(){.icon-spin{height:.9em}.btn .icon-spin{height:auto}.icon-spin.icon-large{height:1.25em}.btn .icon-spin.icon-large{height:.75em}}.icon-glass:before{content:"\f000"}.icon-music:before{content:"\f001"}.icon-search:before{content:"\f002"}.icon-envelope:before{content:"\f003"}.icon-heart:before{content:"\f004"}.icon-star:before{content:"\f005"}.icon-star-empty:before{content:"\f006"}.icon-user:before{content:"\f007"}.icon-film:before{content:"\f008"}.icon-th-large:before{content:"\f009"}.icon-th:before{content:"\f00a"}.icon-th-list:before{content:"\f00b"}.icon-ok:before{content:"\f00c"}.icon-remove:before{content:"\f00d"}.icon-zoom-in:before{content:"\f00e"}.icon-zoom-out:before{content:"\f010"}.icon-off:before{content:"\f011"}.icon-signal:before{content:"\f012"}.icon-cog:before{content:"\f013"}.icon-trash:before{content:"\f014"}.icon-home:before{content:"\f015"}.icon-file:before{content:"\f016"}.icon-time:before{content:"\f017"}.icon-road:before{content:"\f018"}.icon-download-alt:before{content:"\f019"}.icon-download:before{content:"\f01a"}.icon-upload:before{content:"\f01b"}.icon-inbox:before{content:"\f01c"}.icon-play-circle:before{content:"\f01d"}.icon-repeat:before{content:"\f01e"}.icon-refresh:before{content:"\f021"}.icon-list-alt:before{content:"\f022"}.icon-lock:before{content:"\f023"}.icon-flag:before{content:"\f024"}.icon-headphones:before{content:"\f025"}.icon-volume-off:before{content:"\f026"}.icon-volume-down:before{content:"\f027"}.icon-volume-up:before{content:"\f028"}.icon-qrcode:before{content:"\f029"}.icon-barcode:before{content:"\f02a"}.icon-tag:before{content:"\f02b"}.icon-tags:before{content:"\f02c"}.icon-book:before{content:"\f02d"}.icon-bookmark:before{content:"\f02e"}.icon-print:before{content:"\f02f"}.icon-camera:before{content:"\f030"}.icon-font:before{content:"\f031"}.icon-bold:before{content:"\f032"}.icon-italic:before{content:"\f033"}.icon-text-height:before{content:"\f034"}.icon-text-width:before{content:"\f035"}.icon-align-left:before{content:"\f036"}.icon-align-center:before{content:"\f037"}.icon-align-right:before{content:"\f038"}.icon-align-justify:before{content:"\f039"}.icon-list:before{content:"\f03a"}.icon-indent-left:before{content:"\f03b"}.icon-indent-right:before{content:"\f03c"}.icon-facetime-video:before{content:"\f03d"}.icon-picture:before{content:"\f03e"}.icon-pencil:before{content:"\f040"}.icon-map-marker:before{content:"\f041"}.icon-adjust:before{content:"\f042"}.icon-tint:before{content:"\f043"}.icon-edit:before{content:"\f044"}.icon-share:before{content:"\f045"}.icon-check:before{content:"\f046"}.icon-move:before{content:"\f047"}.icon-step-backward:before{content:"\f048"}.icon-fast-backward:before{content:"\f049"}.icon-backward:before{content:"\f04a"}.icon-play:before{content:"\f04b"}.icon-pause:before{content:"\f04c"}.icon-stop:before{content:"\f04d"}.icon-forward:before{content:"\f04e"}.icon-fast-forward:before{content:"\f050"}.icon-step-forward:before{content:"\f051"}.icon-eject:before{content:"\f052"}.icon-chevron-left:before{content:"\f053"}.icon-chevron-right:before{content:"\f054"}.icon-plus-sign:before{content:"\f055"}.icon-minus-sign:before{content:"\f056"}.icon-remove-sign:before{content:"\f057"}.icon-ok-sign:before{content:"\f058"}.icon-question-sign:before{content:"\f059"}.icon-info-sign:before{content:"\f05a"}.icon-screenshot:before{content:"\f05b"}.icon-remove-circle:before{content:"\f05c"}.icon-ok-circle:before{content:"\f05d"}.icon-ban-circle:before{content:"\f05e"}.icon-arrow-left:before{content:"\f060"}.icon-arrow-right:before{content:"\f061"}.icon-arrow-up:before{content:"\f062"}.icon-arrow-down:before{content:"\f063"}.icon-share-alt:before{content:"\f064"}.icon-resize-full:before{content:"\f065"}.icon-resize-small:before{content:"\f066"}.icon-plus:before{content:"\f067"}.icon-minus:before{content:"\f068"}.icon-asterisk:before{content:"\f069"}.icon-exclamation-sign:before{content:"\f06a"}.icon-gift:before{content:"\f06b"}.icon-leaf:before{content:"\f06c"}.icon-fire:before{content:"\f06d"}.icon-eye-open:before{content:"\f06e"}.icon-eye-close:before{content:"\f070"}.icon-warning-sign:before{content:"\f071"}.icon-plane:before{content:"\f072"}.icon-calendar:before{content:"\f073"}.icon-random:before{content:"\f074"}.icon-comment:before{content:"\f075"}.icon-magnet:before{content:"\f076"}.icon-chevron-up:before{content:"\f077"}.icon-chevron-down:before{content:"\f078"}.icon-retweet:before{content:"\f079"}.icon-shopping-cart:before{content:"\f07a"}.icon-folder-close:before{content:"\f07b"}.icon-folder-open:before{content:"\f07c"}.icon-resize-vertical:before{content:"\f07d"}.icon-resize-horizontal:before{content:"\f07e"}.icon-bar-chart:before{content:"\f080"}.icon-twitter-sign:before{content:"\f081"}.icon-facebook-sign:before{content:"\f082"}.icon-camera-retro:before{content:"\f083"}.icon-key:before{content:"\f084"}.icon-cogs:before{content:"\f085"}.icon-comments:before{content:"\f086"}.icon-thumbs-up:before{content:"\f087"}.icon-thumbs-down:before{content:"\f088"}.icon-star-half:before{content:"\f089"}.icon-heart-empty:before{content:"\f08a"}.icon-signout:before{content:"\f08b"}.icon-linkedin-sign:before{content:"\f08c"}.icon-pushpin:before{content:"\f08d"}.icon-external-link:before{content:"\f08e"}.icon-signin:before{content:"\f090"}.icon-trophy:before{content:"\f091"}.icon-github-sign:before{content:"\f092"}.icon-upload-alt:before{content:"\f093"}.icon-lemon:before{content:"\f094"}.icon-phone:before{content:"\f095"}.icon-check-empty:before{content:"\f096"}.icon-bookmark-empty:before{content:"\f097"}.icon-phone-sign:before{content:"\f098"}.icon-twitter:before{content:"\f099"}.icon-facebook:before{content:"\f09a"}.icon-github:before{content:"\f09b"}.icon-unlock:before{content:"\f09c"}.icon-credit-card:before{content:"\f09d"}.icon-rss:before{content:"\f09e"}.icon-hdd:before{content:"\f0a0"}.icon-bullhorn:before{content:"\f0a1"}.icon-bell:before{content:"\f0a2"}.icon-certificate:before{content:"\f0a3"}.icon-hand-right:before{content:"\f0a4"}.icon-hand-left:before{content:"\f0a5"}.icon-hand-up:before{content:"\f0a6"}.icon-hand-down:before{content:"\f0a7"}.icon-circle-arrow-left:before{content:"\f0a8"}.icon-circle-arrow-right:before{content:"\f0a9"}.icon-circle-arrow-up:before{content:"\f0aa"}.icon-circle-arrow-down:before{content:"\f0ab"}.icon-globe:before{content:"\f0ac"}.icon-wrench:before{content:"\f0ad"}.icon-tasks:before{content:"\f0ae"}.icon-filter:before{content:"\f0b0"}.icon-briefcase:before{content:"\f0b1"}.icon-fullscreen:before{content:"\f0b2"}.icon-group:before{content:"\f0c0"}.icon-link:before{content:"\f0c1"}.icon-cloud:before{content:"\f0c2"}.icon-beaker:before{content:"\f0c3"}.icon-cut:before{content:"\f0c4"}.icon-copy:before{content:"\f0c5"}.icon-paper-clip:before{content:"\f0c6"}.icon-save:before{content:"\f0c7"}.icon-sign-blank:before{content:"\f0c8"}.icon-reorder:before{content:"\f0c9"}.icon-list-ul:before{content:"\f0ca"}.icon-list-ol:before{content:"\f0cb"}.icon-strikethrough:before{content:"\f0cc"}.icon-underline:before{content:"\f0cd"}.icon-table:before{content:"\f0ce"}.icon-magic:before{content:"\f0d0"}.icon-truck:before{content:"\f0d1"}.icon-pinterest:before{content:"\f0d2"}.icon-pinterest-sign:before{content:"\f0d3"}.icon-google-plus-sign:before{content:"\f0d4"}.icon-google-plus:before{content:"\f0d5"}.icon-money:before{content:"\f0d6"}.icon-caret-down:before{content:"\f0d7"}.icon-caret-up:before{content:"\f0d8"}.icon-caret-left:before{content:"\f0d9"}.icon-caret-right:before{content:"\f0da"}.icon-columns:before{content:"\f0db"}.icon-sort:before{content:"\f0dc"}.icon-sort-down:before{content:"\f0dd"}.icon-sort-up:before{content:"\f0de"}.icon-envelope-alt:before{content:"\f0e0"}.icon-linkedin:before{content:"\f0e1"}.icon-undo:before{content:"\f0e2"}.icon-legal:before{content:"\f0e3"}.icon-dashboard:before{content:"\f0e4"}.icon-comment-alt:before{content:"\f0e5"}.icon-comments-alt:before{content:"\f0e6"}.icon-bolt:before{content:"\f0e7"}.icon-sitemap:before{content:"\f0e8"}.icon-umbrella:before{content:"\f0e9"}.icon-paste:before{content:"\f0ea"}.icon-lightbulb:before{content:"\f0eb"}.icon-exchange:before{content:"\f0ec"}.icon-cloud-download:before{content:"\f0ed"}.icon-cloud-upload:before{content:"\f0ee"}.icon-user-md:before{content:"\f0f0"}.icon-stethoscope:before{content:"\f0f1"}.icon-suitcase:before{content:"\f0f2"}.icon-bell-alt:before{content:"\f0f3"}.icon-coffee:before{content:"\f0f4"}.icon-food:before{content:"\f0f5"}.icon-file-alt:before{content:"\f0f6"}.icon-building:before{content:"\f0f7"}.icon-hospital:before{content:"\f0f8"}.icon-ambulance:before{content:"\f0f9"}.icon-medkit:before{content:"\f0fa"}.icon-fighter-jet:before{content:"\f0fb"}.icon-beer:before{content:"\f0fc"}.icon-h-sign:before{content:"\f0fd"}.icon-plus-sign-alt:before{content:"\f0fe"}.icon-double-angle-left:before{content:"\f100"}.icon-double-angle-right:before{content:"\f101"}.icon-double-angle-up:before{content:"\f102"}.icon-double-angle-down:before{content:"\f103"}.icon-angle-left:before{content:"\f104"}.icon-angle-right:before{content:"\f105"}.icon-angle-up:before{content:"\f106"}.icon-angle-down:before{content:"\f107"}.icon-desktop:before{content:"\f108"}.icon-laptop:before{content:"\f109"}.icon-tablet:before{content:"\f10a"}.icon-mobile-phone:before{content:"\f10b"}.icon-circle-blank:before{content:"\f10c"}.icon-quote-left:before{content:"\f10d"}.icon-quote-right:before{content:"\f10e"}.icon-spinner:before{content:"\f110"}.icon-circle:before{content:"\f111"}.icon-reply:before{content:"\f112"}.icon-github-alt:before{content:"\f113"}.icon-folder-close-alt:before{content:"\f114"}.icon-folder-open-alt:before{content:"\f115"} -------------------------------------------------------------------------------- /demo-suite/demos/fractal/vm_files/mandelbrot.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "bytes" 19 | "expvar" 20 | "flag" 21 | "fmt" 22 | "image" 23 | "image/color" 24 | "image/draw" 25 | "image/png" 26 | "log" 27 | "math" 28 | "math/cmplx" 29 | "math/rand" 30 | "net/http" 31 | "net/url" 32 | "os" 33 | "runtime" 34 | "strconv" 35 | "strings" 36 | "time" 37 | ) 38 | 39 | var ( 40 | colors [numColors]color.RGBA 41 | logEscape float64 42 | minValue, maxValue float64 43 | debugLog *log.Logger 44 | tileServers []string 45 | ) 46 | 47 | // Publish the host that this data was collected from 48 | var hostnameVar = expvar.NewString("hostname") 49 | 50 | // A Map of URL path -> request count 51 | var requestCounts = expvar.NewMap("requestCounts") 52 | 53 | // A Map of URL path -> total request time in microseconds 54 | var requestTime = expvar.NewMap("requestTime") 55 | 56 | // A Map of 'size' -> request count 57 | var tileCount = expvar.NewMap("tileCount") 58 | 59 | // A Map of 'size' -> total time in microseconds 60 | var tileTime = expvar.NewMap("tileTime") 61 | 62 | const ( 63 | // The number of iterations of the Mandelbrot calculation. 64 | // More iterations mean higher quality at the cost of more CPU time. 65 | iterations = 1000 66 | 67 | // The size of an edge of the tile by default 68 | defaultTileSize = 256 69 | maxTileSize = 1024 70 | 71 | // Size, in pixels, of the mandelbrot set at zoom 0 72 | baseZoomSize = 400 73 | 74 | // Making this value higher will make the colors cycle faster 75 | colorDensity = 50 76 | 77 | // The number of colors that we cycle through 78 | numColors = 5000 79 | 80 | // How many times we run the easing loop. Higher numbers will be sharper 81 | // transitions between color stops. 82 | colorRampEase = 2 83 | 84 | // How much to oversample when generating pixels. The number of values 85 | // calculated per pixel will be this value squared. 86 | pixelOversample = 3 87 | 88 | // The final tile size that actually gets rendered 89 | leafTileSize = 32 90 | 91 | enableDebugLog = false 92 | ) 93 | 94 | // A simple expvar.Var that outputs the time, in seconds, that this server has 95 | // been running. 96 | type UptimeVar struct { 97 | StartTime time.Time 98 | } 99 | 100 | func (v *UptimeVar) String() string { 101 | return strconv.FormatFloat(time.Since(v.StartTime).Seconds(), 'f', 2, 64) 102 | } 103 | 104 | func init() { 105 | runtime.GOMAXPROCS(runtime.NumCPU()) 106 | 107 | hostname, _ := os.Hostname() 108 | hostnameVar.Set(hostname) 109 | 110 | expvar.Publish("uptime", &UptimeVar{time.Now()}) 111 | 112 | if enableDebugLog { 113 | debugLog = log.New(os.Stderr, "DEBUG ", log.LstdFlags) 114 | 115 | } else { 116 | null, _ := os.Open(os.DevNull) 117 | debugLog = log.New(null, "", 0) 118 | } 119 | 120 | minValue = math.MaxFloat64 121 | maxValue = 0 122 | 123 | initColors() 124 | } 125 | 126 | func isPowerOf2(num int) bool { 127 | return (num & (num - 1)) == 0 128 | } 129 | 130 | // The official Google Colors! 131 | var colorStops = []color.Color{ 132 | color.RGBA{0x00, 0x99, 0x25, 0xFF}, // Green 133 | color.RGBA{0x33, 0x69, 0xE8, 0xFF}, // Blue 134 | color.RGBA{0xD5, 0x0F, 0x25, 0xFF}, // Red 135 | color.RGBA{0xEE, 0xB2, 0x11, 0xFF}, // Yellow 136 | color.RGBA{0xFF, 0xFF, 0xFF, 0xFF}, // White 137 | } 138 | 139 | var centerColor = color.RGBA{0x66, 0x66, 0x66, 0xFF} // Gray 140 | 141 | func interpolateColor(c1, c2 color.Color, where float64) color.Color { 142 | r1, g1, b1, a1 := c1.RGBA() 143 | r2, g2, b2, a2 := c2.RGBA() 144 | 145 | var c color.RGBA64 146 | c.R = uint16((float64(r2)-float64(r1))*where + float64(r1) + 0.5) 147 | c.G = uint16((float64(g2)-float64(g1))*where + float64(g1) + 0.5) 148 | c.B = uint16((float64(b2)-float64(b1))*where + float64(b1) + 0.5) 149 | c.A = uint16((float64(a2)-float64(a1))*where + float64(a1) + 0.5) 150 | return c 151 | } 152 | 153 | func initColors() { 154 | cIndex := 0 155 | numColorsLeft := numColors 156 | numStopsLeft := len(colorStops) 157 | prevStop := colorStops[len(colorStops)-1] 158 | for _, stop := range colorStops { 159 | debugLog.Println(stop) 160 | numColorsInStop := numColorsLeft / numStopsLeft 161 | debugLog.Println(numColorsInStop, numColorsLeft, numStopsLeft) 162 | 163 | for i := 0; i < numColorsInStop; i++ { 164 | where := float64(i) / float64(numColorsInStop) 165 | 166 | // This is a sigmoidal-ish easing function as described here: 167 | // http://sol.gfxile.net/interpolation/ 168 | for j := 0; j < colorRampEase; j++ { 169 | where = where * where * (3 - 2*where) 170 | } 171 | //where = math.Pow(where, colorRampEase) 172 | c := interpolateColor(prevStop, stop, where) 173 | colors[cIndex] = color.RGBAModel.Convert(c).(color.RGBA) 174 | cIndex++ 175 | } 176 | 177 | prevStop = stop 178 | numColorsLeft -= numColorsInStop 179 | numStopsLeft-- 180 | } 181 | 182 | for _, c := range colors { 183 | debugLog.Printf("%v", c) 184 | } 185 | } 186 | 187 | func tileHandler(w http.ResponseWriter, r *http.Request) { 188 | w.Header().Set("Access-Control-Allow-Origin", "*") 189 | 190 | x, _ := strconv.Atoi(r.FormValue("x")) 191 | y, _ := strconv.Atoi(r.FormValue("y")) 192 | z, _ := strconv.Atoi(r.FormValue("z")) 193 | tileSize, err := strconv.Atoi(r.FormValue("tile-size")) 194 | if err != nil { 195 | tileSize = defaultTileSize 196 | } 197 | if tileSize <= 0 || tileSize > maxTileSize || !isPowerOf2(tileSize) { 198 | w.WriteHeader(http.StatusBadRequest) 199 | return 200 | } 201 | 202 | t0 := time.Now() 203 | tileCount.Add(strconv.Itoa(tileSize), 1) 204 | 205 | var b []byte 206 | if tileSize > leafTileSize && len(tileServers) > 0 { 207 | b = downloadAndCompositeTiles(x, y, z, tileSize) 208 | } else { 209 | b = renderImage(x, y, z, tileSize) 210 | } 211 | w.Header().Set("Content-Type", "image/png") 212 | w.Header().Set("Content-Length", strconv.Itoa(len(b))) 213 | w.Write(b) 214 | 215 | tileTime.Add(strconv.Itoa(tileSize), time.Since(t0).Nanoseconds()) 216 | } 217 | 218 | func healthHandler(w http.ResponseWriter, r *http.Request) { 219 | w.Header().Set("Content-Type", "text/plain") 220 | w.Header().Set("Access-Control-Allow-Origin", "*") 221 | fmt.Fprintln(w, "ok") 222 | } 223 | 224 | func quitHandler(w http.ResponseWriter, r *http.Request) { 225 | w.Header().Set("Content-Type", "text/plain") 226 | w.Header().Set("Access-Control-Allow-Origin", "*") 227 | fmt.Fprintln(w, "ok") 228 | 229 | log.Println("Exiting process on /debug/quit") 230 | 231 | // Wait 500ms and then exit the process 232 | go func() { 233 | time.Sleep(500 * time.Millisecond) 234 | os.Exit(1) 235 | }() 236 | } 237 | 238 | func resetVarMap(varMap *expvar.Map) { 239 | // There is no easy way to delete/clear expvar.Map. As such there is a slight 240 | // race here. *sigh* 241 | keys := []string{} 242 | varMap.Do(func(kv expvar.KeyValue) { 243 | keys = append(keys, kv.Key) 244 | }) 245 | 246 | for _, key := range keys { 247 | varMap.Set(key, new(expvar.Int)) 248 | } 249 | } 250 | 251 | func varResetHandler(w http.ResponseWriter, r *http.Request) { 252 | resetVarMap(requestCounts) 253 | resetVarMap(requestTime) 254 | resetVarMap(tileCount) 255 | resetVarMap(tileTime) 256 | 257 | w.Header().Set("Content-Type", "text/plain") 258 | w.Header().Set("Access-Control-Allow-Origin", "*") 259 | fmt.Fprintln(w, "ok") 260 | } 261 | 262 | func downloadAndCompositeTiles(x, y, z, tileSize int) []byte { 263 | resultImg := image.NewRGBA(image.Rect(0, 0, tileSize, tileSize)) 264 | 265 | subTileCount := tileSize / leafTileSize 266 | subTileXStart := x * subTileCount 267 | subTileYStart := y * subTileCount 268 | 269 | c := make(chan TileResult) 270 | for subX := subTileXStart; subX < subTileXStart+subTileCount; subX++ { 271 | for subY := subTileYStart; subY < subTileYStart+subTileCount; subY++ { 272 | debugLog.Printf("Spawing goroutine to render x: %v y: %v z: %v", 273 | subX, subY, z) 274 | go func(subX, subY int) { 275 | c <- downloadAndDecodeImage(subX, subY, z, leafTileSize) 276 | }(subX, subY) 277 | } 278 | } 279 | 280 | // Loop to get each image. As they come in composite it into the destination 281 | // image. An alternative would be to composite into the target image in the 282 | // goroutine but that might not be threadsafe. 283 | for i := 0; i < subTileCount*subTileCount; i++ { 284 | result := <-c 285 | if result.img != nil { 286 | debugLog.Printf("Compositing result for x: %v y: %v", result.x, result.y) 287 | localTileOrigin := image.Pt((result.x-subTileXStart)*leafTileSize, 288 | (result.y-subTileYStart)*leafTileSize) 289 | destRect := result.img.Bounds().Add(localTileOrigin) 290 | draw.Draw(resultImg, destRect, result.img, image.ZP, draw.Src) 291 | } else { 292 | debugLog.Printf("No image returned for x: %v y: %v", result.x, result.y) 293 | } 294 | } 295 | 296 | buf := new(bytes.Buffer) 297 | png.Encode(buf, resultImg) 298 | return buf.Bytes() 299 | } 300 | 301 | type TileResult struct { 302 | x int 303 | y int 304 | img *image.RGBA 305 | } 306 | 307 | func downloadAndDecodeImage(x, y, z, tileSize int) TileResult { 308 | tileResult := TileResult{x: x, y: y} 309 | 310 | v := url.Values{} 311 | v.Set("x", strconv.Itoa(x)) 312 | v.Set("y", strconv.Itoa(y)) 313 | v.Set("z", strconv.Itoa(z)) 314 | v.Set("tile-size", strconv.Itoa(tileSize)) 315 | u := url.URL{ 316 | Scheme: "http", 317 | Host: tileServers[rand.Intn(len(tileServers))], 318 | Path: "/tile", 319 | RawQuery: v.Encode(), 320 | } 321 | 322 | // Get the image 323 | debugLog.Println("GETing:", u.String()) 324 | httpResult, err := http.Get(u.String()) 325 | if err != nil { 326 | log.Printf("Error GETing %v: %v", u.String(), err) 327 | return tileResult 328 | } 329 | debugLog.Println("GET success:", u.String()) 330 | 331 | // Decode that puppy 332 | img, _, _ := image.Decode(httpResult.Body) 333 | tileResult.img = img.(*image.RGBA) 334 | httpResult.Body.Close() 335 | 336 | return tileResult 337 | } 338 | 339 | // mandelbrotColor computes a Mandelbrot value and then assigns a color from the 340 | // color table. 341 | func mandelbrotColor(c complex128, zoom int) color.RGBA { 342 | // Scale so we can fit the entire set in one tile when zoomed out. 343 | c = c*3.5 - complex(2.5, 1.75) 344 | 345 | z := complex(0, 0) 346 | iter := 0 347 | for ; iter < iterations; iter++ { 348 | z = z*z + c 349 | r, i := real(z), imag(z) 350 | absSquared := r*r + i*i 351 | if absSquared >= 4 { 352 | // This is the "Continuous (smooth) coloring" described in Wikipedia: 353 | // http://en.wikipedia.org/wiki/Mandelbrot_set#Continuous_.28smooth.29_coloring 354 | v := float64(iter) - math.Log2(math.Log(cmplx.Abs(z))/math.Log(4)) 355 | 356 | // We are scaling the value based on the zoom level so things don't get 357 | // too busy as we get further in. 358 | v = math.Abs(v) * float64(colorDensity) / math.Max(float64(zoom), 1) 359 | minValue = math.Min(float64(v), minValue) 360 | maxValue = math.Max(float64(v), maxValue) 361 | colorIdx := (int(v) + numColors*zoom/len(colorStops)) % numColors 362 | return colors[colorIdx] 363 | } 364 | } 365 | 366 | return centerColor 367 | } 368 | 369 | func renderImage(x, y, z, tileSize int) []byte { 370 | // tileX and tileY is the absolute position of this tile at the current zoom 371 | // level. 372 | numTiles := int(1 << uint(z)) 373 | oversampleTileSize := tileSize * pixelOversample 374 | tileXOrigin, tileYOrigin := x*tileSize*pixelOversample, y*tileSize*pixelOversample 375 | scale := 1 / float64(numTiles*baseZoomSize*pixelOversample) 376 | 377 | debugLog.Printf("Rendering Tile x: %v y: %v z: %v tileSize: %v ", x, y, z, tileSize) 378 | 379 | numPixels := 0 380 | img := image.NewRGBA(image.Rect(0, 0, tileSize, tileSize)) 381 | for tileX := 0; tileX < oversampleTileSize; tileX += pixelOversample { 382 | for tileY := 0; tileY < oversampleTileSize; tileY += pixelOversample { 383 | var r, g, b int32 384 | for dX := 0; dX < pixelOversample; dX++ { 385 | for dY := 0; dY < pixelOversample; dY++ { 386 | c := complex(float64(tileXOrigin+tileX+dX)*scale, 387 | float64(tileYOrigin+tileY+dY)*scale) 388 | // log.Println(c) 389 | clr := mandelbrotColor(c, z) 390 | r += int32(clr.R) 391 | g += int32(clr.G) 392 | b += int32(clr.B) 393 | } 394 | } 395 | img.SetRGBA( 396 | tileX/pixelOversample, 397 | tileY/pixelOversample, 398 | color.RGBA{ 399 | uint8(r / (pixelOversample * pixelOversample)), 400 | uint8(g / (pixelOversample * pixelOversample)), 401 | uint8(b / (pixelOversample * pixelOversample)), 402 | 0xFF}) 403 | 404 | // Every 100 pixels yield the goroutine so other stuff can make progress. 405 | numPixels++ 406 | if numPixels%100 == 0 { 407 | runtime.Gosched() 408 | } 409 | } 410 | } 411 | 412 | debugLog.Printf("Render Done. Value range min: %f, max: %f", minValue, maxValue) 413 | 414 | // Add a sleep to simulate a more complex computation. This scales with the 415 | // number of pixels rendered. 416 | //time.Sleep(time.Duration(tileSize*tileSize/50) * time.Microsecond) 417 | 418 | buf := new(bytes.Buffer) 419 | png.Encode(buf, img) 420 | return buf.Bytes() 421 | } 422 | 423 | // A Request object that collects timing information of all intercepted requests as they 424 | // come in and publishes them to exported vars. 425 | type RequestStatInterceptor struct { 426 | NextHandler http.Handler 427 | } 428 | 429 | func (stats *RequestStatInterceptor) ServeHTTP(w http.ResponseWriter, req *http.Request) { 430 | requestCounts.Add(req.URL.Path, 1) 431 | t0 := time.Now() 432 | stats.NextHandler.ServeHTTP(w, req) 433 | requestTime.Add(req.URL.Path, time.Since(t0).Nanoseconds()) 434 | } 435 | 436 | func main() { 437 | 438 | http.HandleFunc("/health", healthHandler) 439 | http.HandleFunc("/tile", tileHandler) 440 | http.HandleFunc("/debug/quit", quitHandler) 441 | http.HandleFunc("/debug/vars/reset", varResetHandler) 442 | 443 | // Support opening multiple ports so that we aren't bound by HTTP connection 444 | // limits in browsers. 445 | portBase := flag.Int("portBase", 8900, "The base port.") 446 | numPorts := flag.Int("numPorts", 10, "Number of ports to open.") 447 | tileServersArg := flag.String("tileServers", "", 448 | "Downstream tile servers to use when doing composited rendering.") 449 | flag.Parse() 450 | 451 | // Go is super regular with string splits. An empty string results in a list 452 | // with an empty string in it. It is logical but a pain. 453 | tileServers = strings.Split(*tileServersArg, ",") 454 | di, si := 0, 0 455 | for ; si < len(tileServers); si++ { 456 | tileServers[di] = strings.TrimSpace(tileServers[si]) 457 | if len(tileServers[di]) > 0 { 458 | di++ 459 | } 460 | } 461 | tileServers = tileServers[:di] 462 | log.Printf("Tile Servers: %q", tileServers) 463 | 464 | handler := &RequestStatInterceptor{http.DefaultServeMux} 465 | 466 | for i := 0; i < *numPorts; i++ { 467 | portSpec := fmt.Sprintf("0.0.0.0:%v", *portBase+i) 468 | go func() { 469 | log.Println("Listening on", portSpec) 470 | err := http.ListenAndServe(portSpec, handler) 471 | if err != nil { 472 | log.Fatal("ListenAndServe: ", err) 473 | } 474 | }() 475 | } 476 | 477 | select {} 478 | } 479 | -------------------------------------------------------------------------------- /demo-suite/demos/fractal/main.py: -------------------------------------------------------------------------------- 1 | # Copyright 2012 Google Inc. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Fractal demo.""" 16 | 17 | from __future__ import with_statement 18 | 19 | __author__ = 'kbrisbin@google.com (Kathryn Hurley)' 20 | 21 | import json 22 | import logging 23 | import os 24 | import time 25 | 26 | import lib_path 27 | import google_cloud.gce as gce 28 | import google_cloud.gce_appengine as gce_appengine 29 | import google_cloud.oauth as oauth 30 | import jinja2 31 | import oauth2client.appengine as oauth2client 32 | import user_data 33 | import webapp2 34 | 35 | from google.appengine.api import urlfetch 36 | 37 | DEMO_NAME = 'fractal' 38 | CUSTOM_IMAGE = 'fractal-demo-image' 39 | MACHINE_TYPE='n1-highcpu-2' 40 | FIREWALL = 'www-fractal' 41 | FIREWALL_DESCRIPTION = 'Fractal Demo Firewall' 42 | GCE_SCOPE = 'https://www.googleapis.com/auth/compute' 43 | HEALTH_CHECK_TIMEOUT = 1 44 | 45 | VM_FILES = os.path.join(os.path.dirname(__file__), 'vm_files') 46 | STARTUP_SCRIPT = os.path.join(VM_FILES, 'startup.sh') 47 | GO_PROGRAM = os.path.join(VM_FILES, 'mandelbrot.go') 48 | GO_ARGS = '--portBase=80 --numPorts=1' 49 | GO_TILESERVER_FLAG = '--tileServers=' 50 | 51 | jinja_environment = jinja2.Environment(loader=jinja2.FileSystemLoader('')) 52 | oauth_decorator = oauth.decorator 53 | parameters = [ 54 | user_data.DEFAULTS[user_data.GCE_PROJECT_ID], 55 | user_data.DEFAULTS[user_data.GCE_ZONE_NAME], 56 | user_data.DEFAULTS[user_data.GCE_LOAD_BALANCER_IP], 57 | ] 58 | data_handler = user_data.DataHandler(DEMO_NAME, parameters) 59 | 60 | 61 | class ServerVarsAggregator(object): 62 | """Aggregate stats across multiple servers and produce a summary.""" 63 | 64 | def __init__(self): 65 | """Constructor for ServerVarsAggregator.""" 66 | # A map of tile-size -> count 67 | self.tile_counts = {} 68 | # A map of tile-size -> time 69 | self.tile_times = {} 70 | 71 | # The uptime of the server that has been up and running the longest. 72 | self.max_uptime = 0 73 | 74 | def aggregate_vars(self, instance_vars): 75 | """Integrate instance_vars into the running aggregates. 76 | 77 | Args: 78 | instance_vars A parsed JSON object returned from /debug/vars 79 | """ 80 | self._aggregate_map(instance_vars['tileCount'], self.tile_counts) 81 | self._aggregate_map(instance_vars['tileTime'], self.tile_times) 82 | self.max_uptime = max(self.max_uptime, instance_vars['uptime']) 83 | 84 | def _aggregate_map(self, src_map, dest_map): 85 | """Aggregate one map from src_map into dest_map.""" 86 | for k, v in src_map.items(): 87 | dest_map[k] = dest_map.get(k, 0L) + long(v) 88 | 89 | def get_aggregate(self): 90 | """Get the overall aggregate, including derived values.""" 91 | tile_time_avg = {} 92 | result = { 93 | 'tileCount': self.tile_counts.copy(), 94 | 'tileTime': self.tile_times.copy(), 95 | 'tileTimeAvgMs': tile_time_avg, 96 | 'maxUptime': self.max_uptime, 97 | } 98 | for size, count in self.tile_counts.items(): 99 | time = self.tile_times.get(size, 0) 100 | if time and count: 101 | # Compute average tile time in milliseconds. The raw time is in 102 | # nanoseconds. 103 | tile_time_avg[size] = float(time / count) / float(1000*1000) 104 | logging.debug('tile-size: %s count: %d time: %d avg: %d', size, count, time, tile_time_avg[size]) 105 | return result 106 | 107 | 108 | class Fractal(webapp2.RequestHandler): 109 | """Fractal demo.""" 110 | 111 | @oauth_decorator.oauth_required 112 | @data_handler.data_required 113 | def get(self): 114 | """Show main page of Fractal demo.""" 115 | 116 | template = jinja_environment.get_template( 117 | 'demos/%s/templates/index.html' % DEMO_NAME) 118 | data = data_handler.stored_user_data 119 | gce_project_id = data[user_data.GCE_PROJECT_ID] 120 | gce_load_balancer_ip = self._get_lb_servers() 121 | self.response.out.write(template.render({ 122 | 'demo_name': DEMO_NAME, 123 | 'lb_enabled': bool(gce_load_balancer_ip), 124 | 'lb_ip': ', '.join(gce_load_balancer_ip), 125 | })) 126 | 127 | @oauth_decorator.oauth_required 128 | @data_handler.data_required 129 | def get_instances(self): 130 | """List instances. 131 | 132 | Uses app engine app identity to retrieve an access token for the app 133 | engine service account. No client OAuth required. External IP is used 134 | to determine if the instance is actually running. 135 | """ 136 | 137 | gce_project = self._create_gce() 138 | instances = gce_appengine.GceAppEngine().run_gce_request( 139 | self, 140 | gce_project.list_instances, 141 | 'Error listing instances: ', 142 | filter='name eq ^%s-.*' % self.instance_prefix()) 143 | 144 | # A map of instanceName -> (ip, RPC) 145 | health_rpcs = {} 146 | 147 | # Convert instance info to dict and check server status. 148 | num_running = 0 149 | instance_dict = {} 150 | if instances: 151 | for instance in instances: 152 | instance_record = {} 153 | instance_dict[instance.name] = instance_record 154 | if instance.status: 155 | instance_record['status'] = instance.status 156 | else: 157 | instance_record['status'] = 'OTHER' 158 | ip = None 159 | for interface in instance.network_interfaces: 160 | for config in interface.get('accessConfigs', []): 161 | if 'natIP' in config: 162 | ip = config['natIP'] 163 | instance_record['externalIp'] = ip 164 | break 165 | if ip: break 166 | 167 | # Ping the instance server. Grab stats from /debug/vars. 168 | if ip and instance.status == 'RUNNING': 169 | num_running += 1 170 | health_url = 'http://%s/debug/vars?t=%d' % (ip, int(time.time())) 171 | logging.debug('Health checking %s', health_url) 172 | rpc = urlfetch.create_rpc(deadline = HEALTH_CHECK_TIMEOUT) 173 | urlfetch.make_fetch_call(rpc, url=health_url) 174 | health_rpcs[instance.name] = rpc 175 | 176 | # Ping through a LBs too. Only if we get success there do we know we are 177 | # really serving. 178 | loadbalancers = [] 179 | lb_rpcs = {} 180 | if instances and len(instances) > 1: 181 | loadbalancers = self._get_lb_servers() 182 | if num_running > 0 and loadbalancers: 183 | for lb in loadbalancers: 184 | health_url = 'http://%s/health?t=%d' % (lb, int(time.time())) 185 | logging.debug('Health checking %s', health_url) 186 | rpc = urlfetch.create_rpc(deadline = HEALTH_CHECK_TIMEOUT) 187 | urlfetch.make_fetch_call(rpc, url=health_url) 188 | lb_rpcs[lb] = rpc 189 | 190 | # wait for RPCs to complete and update dict as necessary 191 | vars_aggregator = ServerVarsAggregator() 192 | 193 | # TODO: there is significant duplication here. Refactor. 194 | for (instance_name, rpc) in health_rpcs.items(): 195 | result = None 196 | instance_record = instance_dict[instance_name] 197 | try: 198 | result = rpc.get_result() 199 | if result and "memstats" in result.content: 200 | logging.debug('%s healthy!', instance_name) 201 | instance_record['status'] = 'SERVING' 202 | instance_vars = {} 203 | try: 204 | instance_vars = json.loads(result.content) 205 | instance_record['vars'] = instance_vars 206 | vars_aggregator.aggregate_vars(instance_vars) 207 | except ValueError as error: 208 | logging.error('Error decoding vars json for %s: %s', instance_name, error) 209 | else: 210 | logging.debug('%s unhealthy. Content: %s', instance_name, result.content) 211 | except urlfetch.Error as error: 212 | logging.debug('%s unhealthy: %s', instance_name, str(error)) 213 | 214 | # Check health status through the load balancer. 215 | loadbalancer_healthy = bool(lb_rpcs) 216 | for (lb, lb_rpc) in lb_rpcs.items(): 217 | result = None 218 | try: 219 | result = lb_rpc.get_result() 220 | if result and "ok" in result.content: 221 | logging.info('LB %s healthy: %s\n%s', lb, result.headers, result.content) 222 | else: 223 | logging.info('LB %s result not okay: %s, %s', lb, result.status_code, result.content) 224 | loadbalancer_healthy = False 225 | break 226 | except urlfetch.Error as error: 227 | logging.info('LB %s fetch error: %s', lb, str(error)) 228 | loadbalancer_healthy = False 229 | break 230 | 231 | response_dict = { 232 | 'instances': instance_dict, 233 | 'vars': vars_aggregator.get_aggregate(), 234 | 'loadbalancers': loadbalancers, 235 | 'loadbalancer_healthy': loadbalancer_healthy, 236 | } 237 | self.response.headers['Content-Type'] = 'application/json' 238 | self.response.out.write(json.dumps(response_dict)) 239 | 240 | @oauth_decorator.oauth_required 241 | @data_handler.data_required 242 | def set_instances(self): 243 | """Start/stop instances so we have the requested number running.""" 244 | 245 | gce_project = self._create_gce() 246 | 247 | self._setup_firewall(gce_project) 248 | image = self._get_image(gce_project) 249 | disks = self._get_disks(gce_project) 250 | 251 | # Get the list of instances to insert. 252 | num_instances = int(self.request.get('num_instances')) 253 | target = self._get_instance_list( 254 | gce_project, num_instances, image, disks) 255 | target_set = set() 256 | target_map = {} 257 | for instance in target: 258 | target_set.add(instance.name) 259 | target_map[instance.name] = instance 260 | 261 | # Get the list of instances running 262 | current = gce_appengine.GceAppEngine().run_gce_request( 263 | self, 264 | gce_project.list_instances, 265 | 'Error listing instances: ', 266 | filter='name eq ^%s-.*' % self.instance_prefix()) 267 | current_set = set() 268 | current_map = {} 269 | for instance in current: 270 | current_set.add(instance.name) 271 | current_map[instance.name] = instance 272 | 273 | # Add the new instances 274 | to_add_set = target_set - current_set 275 | to_add = [target_map[name] for name in to_add_set] 276 | if to_add: 277 | gce_appengine.GceAppEngine().run_gce_request( 278 | self, 279 | gce_project.bulk_insert, 280 | 'Error inserting instances: ', 281 | resources=to_add) 282 | 283 | # Remove the old instances 284 | to_remove_set = current_set - target_set 285 | to_remove = [current_map[name] for name in to_remove_set] 286 | if to_remove: 287 | gce_appengine.GceAppEngine().run_gce_request( 288 | self, 289 | gce_project.bulk_delete, 290 | 'Error deleting instances: ', 291 | resources=to_remove) 292 | 293 | logging.info("current_set: %s", current_set) 294 | logging.info("target_set: %s", target_set) 295 | logging.info("to_add_set: %s", to_add_set) 296 | logging.info("to_remove_set: %s", to_remove_set) 297 | 298 | @oauth_decorator.oauth_required 299 | @data_handler.data_required 300 | def cleanup(self): 301 | """Stop instances using the gce_appengine helper class.""" 302 | gce_project = self._create_gce() 303 | gce_appengine.GceAppEngine().delete_demo_instances( 304 | self, gce_project, self.instance_prefix()) 305 | 306 | def _get_lb_servers(self): 307 | data = data_handler.stored_user_data 308 | return data.get(user_data.GCE_LOAD_BALANCER_IP, []) 309 | 310 | def instance_prefix(self): 311 | """Return a prefix based on a request/query params.""" 312 | tag = self.request.get('tag') 313 | prefix = DEMO_NAME 314 | if tag: 315 | prefix = prefix + '-' + tag 316 | return prefix 317 | 318 | def _create_gce(self): 319 | gce_project_id = data_handler.stored_user_data[user_data.GCE_PROJECT_ID] 320 | gce_zone_name = data_handler.stored_user_data[user_data.GCE_ZONE_NAME] 321 | return gce.GceProject(oauth_decorator.credentials, 322 | project_id=gce_project_id, 323 | zone_name=gce_zone_name) 324 | 325 | def _setup_firewall(self, gce_project): 326 | "Create the firewall if it doesn't exist." 327 | firewalls = gce_project.list_firewalls() 328 | firewall_names = [firewall.name for firewall in firewalls] 329 | if not FIREWALL in firewall_names: 330 | firewall = gce.Firewall( 331 | name=FIREWALL, 332 | target_tags=[DEMO_NAME], 333 | description=FIREWALL_DESCRIPTION) 334 | gce_project.insert(firewall) 335 | 336 | def _get_image(self, gce_project): 337 | """Returns the appropriate image to use. def _has_custom_image(self, gce_project): 338 | 339 | Args: 340 | gce_project: An instance of gce.GceProject 341 | 342 | Returns: (project, image_name) for the image to use. 343 | """ 344 | images = gce_project.list_images(filter='name eq ^%s$' % CUSTOM_IMAGE) 345 | if images: 346 | return (gce_project.project_id, CUSTOM_IMAGE) 347 | return ('google', None) 348 | 349 | def _get_disks(self, gce_project): 350 | """Get boot disks for VMs.""" 351 | disks_array = gce_project.list_disks( 352 | filter='name eq ^boot-%s-.*' % self.instance_prefix()) 353 | 354 | disks = {} 355 | for d in disks_array: 356 | disks[d.name] = d 357 | return disks 358 | 359 | def _get_instance_metadata(self, gce_project, instance_names): 360 | """The metadata values to pass into the instance.""" 361 | inline_values = { 362 | 'goargs': GO_ARGS, 363 | } 364 | 365 | file_values = { 366 | 'startup-script': STARTUP_SCRIPT, 367 | 'goprog': GO_PROGRAM, 368 | } 369 | 370 | # Try and use LBs if we have any. But only do that if we have more than one 371 | # instance. 372 | if instance_names: 373 | tile_servers = '' 374 | if len(instance_names) > 1: 375 | tile_servers = self._get_lb_servers() 376 | if not tile_servers: 377 | tile_servers = instance_names 378 | tile_servers = ','.join(tile_servers) 379 | inline_values['goargs'] += ' %s%s' %(GO_TILESERVER_FLAG, tile_servers) 380 | 381 | metadata = [] 382 | for k, v in inline_values.items(): 383 | metadata.append({'key': k, 'value': v}) 384 | 385 | for k, fv in file_values.items(): 386 | v = open(fv, 'r').read() 387 | metadata.append({'key': k, 'value': v}) 388 | return metadata 389 | 390 | def _get_instance_list(self, gce_project, num_instances, image, disks): 391 | """Get a list of instances to start. 392 | 393 | Args: 394 | gce_project: An instance of gce.GceProject. 395 | num_instances: The number of instances to start. 396 | image: tuple with (project_name, image_name) for the image to use. 397 | disks: A dictionary of disk_name -> disk resources 398 | 399 | Returns: 400 | A list of gce.Instances. 401 | """ 402 | 403 | 404 | instance_names = [] 405 | for i in range(num_instances): 406 | instance_names.append('%s-%02d' % (self.instance_prefix(), i)) 407 | 408 | instance_list = [] 409 | for instance_name in instance_names: 410 | disk_name = 'boot-%s' % instance_name 411 | disk_mounts = [gce.DiskMount(init_disk_name=disk_name, boot=True, auto_delete=True)] 412 | 413 | gce_zone_name = data_handler.stored_user_data[user_data.GCE_ZONE_NAME] 414 | # Define a network interfaces list here that requests an ephemeral 415 | # external IP address. We will apply this configuration to all VMs 416 | # started by the fractal app. 417 | network = gce.Network('default') 418 | network.gce_project = gce_project 419 | ext_net = [{ 'network': network.url, 420 | 'accessConfigs': [{ 'name': 'External IP access config', 421 | 'type': 'ONE_TO_ONE_NAT' 422 | }] 423 | }] 424 | instance = gce.Instance( 425 | name=instance_name, 426 | machine_type_name=MACHINE_TYPE, 427 | zone_name=gce_zone_name, 428 | network_interfaces=ext_net, 429 | disk_mounts=disk_mounts, 430 | tags=[DEMO_NAME, self.instance_prefix()], 431 | metadata=self._get_instance_metadata(gce_project, instance_names), 432 | service_accounts=gce_project.settings['cloud_service_account']) 433 | instance_list.append(instance) 434 | return instance_list 435 | 436 | 437 | app = webapp2.WSGIApplication( 438 | [ 439 | ('/%s' % DEMO_NAME, Fractal), 440 | webapp2.Route('/%s/instance' % DEMO_NAME, 441 | handler=Fractal, handler_method='get_instances', 442 | methods=['GET']), 443 | webapp2.Route('/%s/instance' % DEMO_NAME, 444 | handler=Fractal, handler_method='set_instances', 445 | methods=['POST']), 446 | webapp2.Route('/%s/cleanup' % DEMO_NAME, 447 | handler=Fractal, handler_method='cleanup', 448 | methods=['POST']), 449 | (data_handler.url_path, data_handler.data_handler), 450 | ], debug=True) 451 | -------------------------------------------------------------------------------- /demo-suite/demos/fractal/static/js/script.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2012 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @fileoverview Fractal demo JavaScript code. 17 | * 18 | * Displays zoomable, panable fractal images, comparing tile load of 1 19 | * instance to 16. 20 | */ 21 | 22 | var fractal1; 23 | var fractalCluster; 24 | 25 | $(document).ready(function() { 26 | $('.btn').button(); 27 | configSpinner(); 28 | fractalCluster = new Fractal($('#fractalCluster'), CLUSTER_INSTANCE_TAG, 29 | NUM_CLUSTER_INSTANCES_START); 30 | fractalCluster.initialize(); 31 | fractal1 = new Fractal($('#fractal1'), SIGNLE_INSTANCE_TAG, 32 | 1, fractalCluster); 33 | fractal1.initialize(); 34 | 35 | $('#start').click(function() { 36 | fractal1.start(); 37 | fractalCluster.start(); 38 | }); 39 | $('#reset').click(function() { 40 | if (fractal1.map) { 41 | toggleMaps(); 42 | } 43 | fractal1.reset(); 44 | fractalCluster.reset(); 45 | }); 46 | $('#clearVars').click(function() { 47 | fractal1.clearVars(); 48 | fractalCluster.clearVars(); 49 | }) 50 | $('#addServer').click(function() { 51 | fractalCluster.deltaServers(+1); 52 | }) 53 | $('#removeServer').click(function() { 54 | fractalCluster.deltaServers(-1); 55 | }) 56 | $('#randomPoi').click(gotoRandomPOI); 57 | $('#toggleMaps').click(toggleMaps); 58 | }); 59 | 60 | /** 61 | * Name for 'slow' map. 62 | * @type {string} 63 | * @constant 64 | */ 65 | var SIGNLE_INSTANCE_TAG = 'single'; 66 | 67 | /** 68 | * Number of instances to start with in the cluster. 69 | * @type {number} 70 | * @constant 71 | */ 72 | var NUM_CLUSTER_INSTANCES_START = 8; 73 | 74 | /** 75 | * Tag for cluster instances. 76 | * @type {string} 77 | * @constant 78 | */ 79 | var CLUSTER_INSTANCE_TAG = 'cluster'; 80 | 81 | 82 | /** 83 | * Configure spinner to show when there is an outstanding Ajax request. 84 | * 85 | * This really helps to show that something is going on. It is the 86 | * simplest blinking light that we can add. 87 | */ 88 | function configSpinner() { 89 | $('#spinner') 90 | .css('visibility', 'hidden') 91 | .ajaxStart(function() { 92 | $('#spinner').css('visibility', ''); 93 | }) 94 | .ajaxStop(function() { 95 | $('#spinner').css('visibility', 'hidden'); 96 | }); 97 | } 98 | 99 | var POINTS_OF_INTEREST = [ 100 | { x: -56.18426015515269, y: 87.95310974121094, z: 13 }, 101 | { x: -55.06490220044015, y: 83.02677154541016, z: 12 }, 102 | { x: -56.20683602602539, y: 87.77841478586197, z: 18 }, 103 | { x: -56.18445122198682, y: 87.96031951904297, z: 18 }, 104 | { x: 4.041501376702832, y: 187.31689453125, z: 12 }, 105 | { x: 39.91121803996906, y: 204.35609936714172, z: 21 }, 106 | ]; 107 | 108 | /** 109 | * Go to a random point of interest on the maps. 110 | */ 111 | function gotoRandomPOI () { 112 | poi = POINTS_OF_INTEREST[Math.floor(Math.random() * POINTS_OF_INTEREST.length)]; 113 | fractal1.map.setCenter(new google.maps.LatLng(poi.x, poi.y, true)); 114 | fractal1.map.setZoom(poi.z); 115 | } 116 | 117 | function toggleMaps() { 118 | if (fractal1.map) { 119 | fractal1.hideMap(); 120 | fractalCluster.hideMap(); 121 | $('#toggleMaps').text('Show Maps') 122 | } else { 123 | fractal1.showMap(); 124 | fractalCluster.showMap(); 125 | $('#toggleMaps').text('Hide Maps') 126 | addListeners(); 127 | } 128 | } 129 | 130 | /** 131 | * Add listeners to maps so that they zoom and pan in unison. 132 | */ 133 | function addListeners() { 134 | if (fractal1.map && fractalCluster.map) { 135 | // Add listeners to the map on the left so that the zoom and center 136 | // is reflected on both maps. 137 | google.maps.event.addListener(fractal1.map, 'zoom_changed', function() { 138 | var zoom = fractal1.map.getZoom(); 139 | fractalCluster.map.setZoom(zoom); 140 | }); 141 | google.maps.event.addListener(fractal1.map, 'center_changed', function() { 142 | var center = fractal1.map.getCenter(); 143 | fractalCluster.map.setCenter(center); 144 | }); 145 | } 146 | }; 147 | 148 | 149 | /** 150 | * Fractal class. 151 | * @param {Element} container HTML element in which to display the map. 152 | * @param {string} tag A unique string ('single') used to identify instances 153 | * @param {number} num_instances Number of instances to start 154 | * position and zoom to. 155 | * @constructor 156 | */ 157 | var Fractal = function(container, tag, num_instances) { 158 | /** 159 | * An HTML object that will contain this fractal map. 160 | * @type {Element} 161 | * @private 162 | */ 163 | this.container_ = container; 164 | 165 | /** 166 | * The element that holds the map itself. 167 | * @type {Element} 168 | * @private 169 | */ 170 | this.mapContainer_ = null; 171 | 172 | /** 173 | * The squares object that will track the state of the VMs. 174 | * @type {Squares} 175 | * @private 176 | */ 177 | this.squares_ = null; 178 | 179 | /** 180 | * A unique string to use for naming instances. Also used as a user visible 181 | * label on the map. 182 | * @type {string} 183 | * @private 184 | */ 185 | this.tag_ = tag; 186 | 187 | /** 188 | * The GCE control object. 189 | * @type {GCE} 190 | * @private 191 | */ 192 | this.gce_ = null; 193 | 194 | /** 195 | * The Map control object 196 | * @type {Map} 197 | */ 198 | this.map = null; 199 | 200 | /** 201 | * The number of instances to launch for this map. 202 | * @type {[type]} 203 | * @private 204 | */ 205 | this.num_instances_ = num_instances; 206 | 207 | /** 208 | * The list of IPs that are serving. 209 | * @type {Array} 210 | */ 211 | this.ips_ = []; 212 | 213 | /** 214 | * The last data returned from the server. Useful for async actions that must 215 | * interact with individual servers directly. 216 | * @type {Object} 217 | */ 218 | this.last_data_ = {}; 219 | 220 | /** 221 | * If this is true then there is a start_instances_ currently running. 222 | * @type {Boolean} 223 | * @private 224 | */ 225 | this.start_in_progress_ = false; 226 | 227 | /** 228 | * If this is true then when the current start_instances_ completes another 229 | * should be scheduled. 230 | * @type {Boolean} 231 | * @private 232 | */ 233 | this.need_another_start_ = false; 234 | }; 235 | 236 | /** 237 | * The map center latitude. 238 | * @type {number} 239 | * @private 240 | */ 241 | Fractal.prototype.LATITUDE_ = -78.35; 242 | 243 | /** 244 | * The map center longitude. 245 | * @type {number} 246 | * @private 247 | */ 248 | Fractal.prototype.LONGITUDE_ = 157.5; 249 | 250 | /** 251 | * The default tile size 252 | * @type {Number} 253 | * @private 254 | */ 255 | Fractal.prototype.TILE_SIZE_ = 128; 256 | 257 | /** 258 | * The minimum zoom on the map 259 | * @type {Number} 260 | * @private 261 | */ 262 | Fractal.prototype.MIN_ZOOM_ = 0; 263 | 264 | /** 265 | * The maximum zoom of the map. 266 | * @type {Number} 267 | * @private 268 | */ 269 | Fractal.prototype.MAX_ZOOM_ = 30; 270 | 271 | /** 272 | * The maximum number of instances we let you start 273 | * @type {Number} 274 | * @private 275 | */ 276 | Fractal.prototype.MAX_INSTANCES_ = 16; 277 | 278 | /** 279 | * Initialize the UI and check if there are instances already up. 280 | */ 281 | Fractal.prototype.initialize = function() { 282 | // Set up the DOM under container_ 283 | var squaresRow = $('
').addClass('row-fluid').addClass('squares-row'); 284 | var squaresContainer = $('
').addClass('span8').addClass('squares'); 285 | squaresRow.append(squaresContainer); 286 | $(this.container_).append(squaresRow); 287 | 288 | var mapRow = $('
').addClass('row-fluid').addClass('map-row'); 289 | $(this.container_).append(mapRow); 290 | 291 | this.squares_ = new Squares( 292 | squaresContainer.get(0), [], { 293 | cols: 8 294 | }); 295 | this.updateSquares_(); 296 | 297 | var statContainer = $('
').addClass('span4'); 298 | squaresRow.append(statContainer); 299 | this.statDisplay_ = new StatDisplay(statContainer, 'Avg Render Time', 'ms', 300 | function (data) { 301 | var vars = data['vars'] || {}; 302 | var avg_render_time = vars['tileTimeAvgMs'] || {}; 303 | return avg_render_time[this.TILE_SIZE_]; 304 | }.bind(this)); 305 | 306 | // DEMO_NAME is set in the index.html template file. 307 | this.gce_ = new Gce('/' + DEMO_NAME + '/instance', 308 | '/' + DEMO_NAME + '/instance', 309 | '/' + DEMO_NAME + '/cleanup', 310 | null, { 311 | 'tag': this.tag_ 312 | }); 313 | this.gce_.setOptions({ 314 | squares: this.squares_, 315 | statDisplay: this.statDisplay_, 316 | }); 317 | 318 | this.gce_.startContinuousHeartbeat(this.heartbeat.bind(this)) 319 | } 320 | 321 | /** 322 | * Handle the heartbeat from the GCE object. 323 | * 324 | * If things are looking good, show the map, otherwise destroy it. 325 | * 326 | * @param {Object} data Result of a server status query 327 | */ 328 | Fractal.prototype.heartbeat = function(data) { 329 | console.log("heartbeat:", data); 330 | 331 | this.last_data_ = data; 332 | this.ips_ = this.getIps_(data); 333 | 334 | this.updateSquares_(); 335 | 336 | var lbs = data['loadbalancers']; 337 | if (lbs && lbs.length > 0) { 338 | if (data['loadbalancer_healthy']) { 339 | $('#lbsOk').css('visibility', 'visible'); 340 | } else { 341 | $('#lbsOk').css('visibility', 'hidden'); 342 | } 343 | } 344 | }; 345 | 346 | Fractal.prototype.clearVars = function() { 347 | var instances = this.last_data_['instances'] || {}; 348 | for (var instanceName in instances) { 349 | ip = instances[instanceName]['externalIp']; 350 | if (ip) { 351 | $.ajax('http://' + ip + '/debug/vars/reset', { 352 | type: 'POST', 353 | }); 354 | } 355 | } 356 | 357 | }; 358 | 359 | /** 360 | * Start up the instances if necessary. When the instances are confirmed to be 361 | * running then show the map. 362 | */ 363 | Fractal.prototype.start = function() { 364 | this.startInstances_(); 365 | }; 366 | 367 | /** 368 | * Reset the map. Shut down the instances and clear the map. 369 | */ 370 | Fractal.prototype.reset = function() { 371 | this.gce_.stopInstances(); 372 | }; 373 | 374 | /** 375 | * Change the number of target servers by delta 376 | * @param {number} delta The number of servers to change the target by 377 | */ 378 | Fractal.prototype.deltaServers = function(delta) { 379 | this.num_instances_ += delta; 380 | this.num_instances_ = Math.max(this.num_instances_, 0); 381 | this.num_instances_ = Math.min(this.num_instances_, this.MAX_INSTANCES_); 382 | 383 | this.updateSquares_(); 384 | this.startInstances_(); 385 | }; 386 | 387 | /** 388 | * Start/stop any instances that need to be started/stopped. This won't have 389 | * more than one start API call outstanding at a time. If one is already 390 | * running it will remember an start another after that one is complete. 391 | */ 392 | Fractal.prototype.startInstances_ = function() { 393 | if (this.start_in_progress_) { 394 | this.need_another_start_ = true; 395 | } else { 396 | this.start_in_progress_ = true; 397 | this.gce_.startInstances(this.num_instances_, { 398 | data: { 399 | 'num_instances': this.num_instances_ 400 | }, 401 | ajaxComplete: function() { 402 | this.start_in_progress_ = false; 403 | if (this.need_another_start_) { 404 | this.need_another_start_ = false; 405 | this.startInstances_(); 406 | } 407 | }.bind(this), 408 | }) 409 | } 410 | } 411 | 412 | Fractal.prototype.updateSquares_ = function() { 413 | // Initialize the squares to the target instances and any existing instances 414 | var instanceMap = {}; 415 | for (var i = 0; i < this.num_instances_; i++) { 416 | var instanceName = DEMO_NAME + '-' + this.tag_ + '-' + padNumber(i, 2); 417 | instanceMap[instanceName] = 1; 418 | } 419 | if (this.last_data_) { 420 | var current_instances = this.last_data_['instances'] || {}; 421 | for (var instanceName in current_instances) { 422 | instanceMap[instanceName] = 1; 423 | } 424 | } 425 | var instanceNames = Object.keys(instanceMap).sort(); 426 | 427 | // Get the current squares and then compare. 428 | var currentSquares = this.squares_.getInstanceNames().sort() 429 | 430 | if (!arraysEqual(instanceNames, currentSquares)) { 431 | this.squares_.resetInstanceNames(instanceNames); 432 | this.squares_.drawSquares(); 433 | if (this.last_data_) { 434 | this.squares_.update(this.last_data_) 435 | } 436 | } 437 | }; 438 | 439 | /** 440 | * Try to cleanup/delete any running map 441 | */ 442 | Fractal.prototype.hideMap = function() { 443 | if (this.map) { 444 | this.map.unbindAll(); 445 | this.map = null 446 | } 447 | if (this.mapContainer_) { 448 | $(this.mapContainer_).remove(); 449 | this.mapContainer_ = null; 450 | } 451 | } 452 | 453 | /** 454 | * Create maps and add listeners to maps. 455 | * @private 456 | */ 457 | Fractal.prototype.showMap = function() { 458 | if (!this.map) { 459 | this.hideMap(); 460 | this.map = this.prepMap_(); 461 | } 462 | }; 463 | 464 | /** 465 | * Set map options and draw a map on HTML page. 466 | * @param {Array.} ips An array of IPs. 467 | * @return {google.maps.Map} Returns the map object. 468 | * @private 469 | */ 470 | Fractal.prototype.prepMap_ = function() { 471 | var that = this; 472 | var fractalTypeOptions = { 473 | getTileUrl: function(coord, zoom) { 474 | var url = ['http://']; 475 | num_serving = that.ips_.length 476 | var instanceIdx = 477 | Math.abs(Math.round(coord.x * Math.sqrt(num_serving) + coord.y)) 478 | % num_serving; 479 | url.push(that.ips_[instanceIdx]); 480 | 481 | var params = { 482 | z: zoom, 483 | x: coord.x, 484 | y: coord.y, 485 | 'tile-size': that.TILE_SIZE_, 486 | }; 487 | url.push('/tile?'); 488 | url.push($.param(params)); 489 | 490 | return url.join(''); 491 | }, 492 | tileSize: new google.maps.Size(this.TILE_SIZE_, this.TILE_SIZE_), 493 | maxZoom: this.MAX_ZOOM_, 494 | minZoom: this.MIN_ZOOM_, 495 | name: 'Mandelbrot', 496 | }; 497 | 498 | this.mapContainer_ = $('
'); 499 | this.mapContainer_.addClass('span12'); 500 | this.mapContainer_.addClass('map-container'); 501 | $(this.container_).find('.map-row').append(this.mapContainer_); 502 | var map = this.drawMap_(this.mapContainer_, 503 | fractalTypeOptions, 'Mandelbrot'); 504 | return map; 505 | }; 506 | 507 | /** 508 | * Get the external IPs of the instances from the returned data. 509 | * @param {Object} data Data returned from the list instances call to GCE. 510 | * @return {Object} The list of ips. 511 | * @private 512 | */ 513 | Fractal.prototype.getIps_ = function(data) { 514 | lbs = data['loadbalancers'] || [] 515 | if (lbs.length > 0) { 516 | return lbs 517 | } else { 518 | var ips = []; 519 | for (var instanceName in data['instances']) { 520 | ip = data['instances'][instanceName]['externalIp']; 521 | if (ip) { 522 | ips.push(ip); 523 | } 524 | } 525 | return ips; 526 | } 527 | }; 528 | 529 | /** 530 | * Draw the map. 531 | * @param {JQuery} canvas The HTML element in which to display the map. 532 | * @param {Object} fractalTypeOptions Options for displaying the map. 533 | * @param {string} mapTypeId A unique map type id. 534 | * @return {google.maps.Map} Returns the map object. 535 | * @private 536 | */ 537 | Fractal.prototype.drawMap_ = function(canvas, fractalTypeOptions, mapTypeId) { 538 | var fractalMapType = new ThrottledImageMap(fractalTypeOptions); 539 | 540 | var mapOptions = { 541 | center: new google.maps.LatLng(this.LATITUDE_, this.LONGITUDE_), 542 | zoom: this.MIN_ZOOM_, 543 | streetViewControl: false, 544 | mapTypeControlOptions: { 545 | mapTypeIds: [mapTypeId] 546 | }, 547 | zoomControlOptions: { 548 | style: google.maps.ZoomControlStyle.SMALL 549 | } 550 | }; 551 | 552 | var map = new google.maps.Map(canvas.get(0), mapOptions); 553 | map.mapTypes.set(mapTypeId, fractalMapType); 554 | map.setMapTypeId(mapTypeId); 555 | return map; 556 | }; 557 | 558 | 559 | /** 560 | * Simply shows a summary stat. 561 | * @param {Node} container The container to render into. 562 | * @param {string} display_name User visible description 563 | * @param {string} units Units of metric. 564 | * @param {function} stat_name A function to return the stat value from a JSON data object. 565 | */ 566 | var StatDisplay = function(container, display_name, units, stat_func) { 567 | this.stat_func = stat_func; 568 | 569 | container = $(container); 570 | 571 | // Render the subtree 572 | var stat_container = $('
').addClass('stat-container'); 573 | container.append(stat_container); 574 | 575 | var stat_name_div = $('
').addClass('stat-name').text(display_name); 576 | stat_container.append(stat_name_div); 577 | 578 | var value_row = $('
').addClass('stat-value-row'); 579 | 580 | this.value_span = $('').addClass('stat-value').text('--'); 581 | value_row.append(this.value_span); 582 | 583 | var value_units = $('').addClass('stat-units').text(units); 584 | value_row.append(value_units); 585 | 586 | stat_container.append(value_row); 587 | } 588 | 589 | StatDisplay.prototype.update = function(data) { 590 | value = this.stat_func(data); 591 | if (value == undefined) { 592 | value = '--'; 593 | } else { 594 | value = value.toFixed(1); 595 | } 596 | this.value_span.text(value); 597 | }; 598 | 599 | 600 | 601 | --------------------------------------------------------------------------------