')
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 |
--------------------------------------------------------------------------------