root / ui / static / snf / js / models.js @ 47276ec2
History | View | Annotate | Download (61.1 kB)
1 |
;(function(root){
|
---|---|
2 |
|
3 |
// root
|
4 |
var root = root;
|
5 |
|
6 |
// setup namepsaces
|
7 |
var snf = root.synnefo = root.synnefo || {};
|
8 |
var models = snf.models = snf.models || {}
|
9 |
var storage = snf.storage = snf.storage || {};
|
10 |
var util = snf.util = snf.util || {};
|
11 |
|
12 |
// shortcuts
|
13 |
var bb = root.Backbone;
|
14 |
var slice = Array.prototype.slice
|
15 |
|
16 |
// logging
|
17 |
var logger = new snf.logging.logger("SNF-MODELS"); |
18 |
var debug = _.bind(logger.debug, logger);
|
19 |
|
20 |
// get url helper
|
21 |
var getUrl = function(baseurl) { |
22 |
var baseurl = baseurl || snf.config.api_url;
|
23 |
return baseurl + "/" + this.path; |
24 |
} |
25 |
|
26 |
// i18n
|
27 |
BUILDING_MESSAGES = window.BUILDING_MESSAGES || {'INIT': 'init', 'COPY': '{0}, {1}, {2}', 'FINAL': 'final'}; |
28 |
|
29 |
// Base object for all our models
|
30 |
models.Model = bb.Model.extend({ |
31 |
sync: snf.api.sync,
|
32 |
api: snf.api,
|
33 |
has_status: false, |
34 |
|
35 |
initialize: function() { |
36 |
if (this.has_status) { |
37 |
this.bind("change:status", this.handle_remove); |
38 |
this.handle_remove();
|
39 |
} |
40 |
models.Model.__super__.initialize.apply(this, arguments) |
41 |
}, |
42 |
|
43 |
handle_remove: function() { |
44 |
if (this.get("status") == 'DELETED') { |
45 |
if (this.collection) { |
46 |
try { this.clear_pending_action();} catch (err) {}; |
47 |
try { this.reset_pending_actions();} catch (err) {}; |
48 |
try { this.stop_stats_update();} catch (err) {}; |
49 |
this.collection.remove(this.id); |
50 |
} |
51 |
} |
52 |
}, |
53 |
|
54 |
// custom set method to allow submodels to use
|
55 |
// set_<attr> methods for handling the value of each
|
56 |
// attribute and overriding the default set method
|
57 |
// for specific parameters
|
58 |
set: function(params, options) { |
59 |
_.each(params, _.bind(function(value, key){
|
60 |
if (this["set_" + key]) { |
61 |
params[key] = this["set_" + key](value); |
62 |
} |
63 |
}, this))
|
64 |
var ret = bb.Model.prototype.set.call(this, params, options); |
65 |
return ret;
|
66 |
}, |
67 |
|
68 |
url: function(options) { |
69 |
return getUrl.call(this, this.base_url) + "/" + this.id; |
70 |
}, |
71 |
|
72 |
api_path: function(options) { |
73 |
return this.path + "/" + this.id; |
74 |
}, |
75 |
|
76 |
parse: function(resp, xhr) { |
77 |
return resp.server;
|
78 |
}, |
79 |
|
80 |
remove: function() { |
81 |
this.api.call(this.api_path(), "delete"); |
82 |
}, |
83 |
|
84 |
changedKeys: function() { |
85 |
return _.keys(this.changedAttributes() || {}); |
86 |
}, |
87 |
|
88 |
hasOnlyChange: function(keys) { |
89 |
var ret = false; |
90 |
_.each(keys, _.bind(function(key) {
|
91 |
if (this.changedKeys().length == 1 && this.changedKeys().indexOf(key) > -1) { ret = true}; |
92 |
}, this));
|
93 |
return ret;
|
94 |
} |
95 |
|
96 |
}) |
97 |
|
98 |
// Base object for all our model collections
|
99 |
models.Collection = bb.Collection.extend({ |
100 |
sync: snf.api.sync,
|
101 |
api: snf.api,
|
102 |
supportIncUpdates: true, |
103 |
url: function(options) { |
104 |
return getUrl.call(this, this.base_url) + (options.details || this.details ? '/detail' : ''); |
105 |
}, |
106 |
|
107 |
fetch: function(options) { |
108 |
if (!options) { options = {} };
|
109 |
// default to update
|
110 |
if (!this.noUpdate) { |
111 |
if (options.update === undefined) { options.update = true }; |
112 |
if (!options.removeMissing && options.refresh) { options.removeMissing = true }; |
113 |
} else {
|
114 |
if (options.refresh === undefined) { |
115 |
options.refresh = true;
|
116 |
} |
117 |
} |
118 |
// custom event foreach fetch
|
119 |
return bb.Collection.prototype.fetch.call(this, options) |
120 |
}, |
121 |
|
122 |
create: function(model, options) { |
123 |
var coll = this; |
124 |
options || (options = {}); |
125 |
model = this._prepareModel(model, options);
|
126 |
if (!model) return false; |
127 |
var success = options.success;
|
128 |
options.success = function(nextModel, resp, xhr) { |
129 |
if (success) success(nextModel, resp, xhr);
|
130 |
}; |
131 |
model.save(null, options);
|
132 |
return model;
|
133 |
}, |
134 |
|
135 |
get_fetcher: function(timeout, fast, limit, initial, params) { |
136 |
var fetch_params = params || {};
|
137 |
fetch_params.skips_timeouts = true;
|
138 |
|
139 |
var timeout = parseInt(timeout);
|
140 |
var fast = fast || 1000; |
141 |
var limit = limit;
|
142 |
var initial_call = initial || true; |
143 |
|
144 |
var last_ajax = undefined; |
145 |
var cb = _.bind(function() { |
146 |
// clone to avoid referenced objects
|
147 |
var params = _.clone(fetch_params);
|
148 |
updater._ajax = last_ajax; |
149 |
if (last_ajax) {
|
150 |
last_ajax.abort(); |
151 |
} |
152 |
last_ajax = this.fetch(params);
|
153 |
}, this);
|
154 |
var updater = new snf.api.updateHandler({'callback': cb, timeout:timeout, |
155 |
fast:fast, limit:limit, |
156 |
call_on_start:initial_call});
|
157 |
|
158 |
snf.api.bind("call", _.throttle(_.bind(function(){ updater.faster(true)}, this)), 1000); |
159 |
return updater;
|
160 |
} |
161 |
}); |
162 |
|
163 |
// Image model
|
164 |
models.Image = models.Model.extend({ |
165 |
path: 'images', |
166 |
|
167 |
get_size: function() { |
168 |
return parseInt(this.get('metadata') ? this.get('metadata').values.size : -1) |
169 |
}, |
170 |
|
171 |
get_readable_size: function() { |
172 |
return this.get_size() > 0 ? util.readablizeBytes(this.get_size() * 1024 * 1024) : "unknown"; |
173 |
}, |
174 |
|
175 |
get_os: function() { |
176 |
return this.get("OS"); |
177 |
}, |
178 |
|
179 |
get_created_user: function() { |
180 |
return synnefo.config.os_created_users[this.get_os()] || "root"; |
181 |
}, |
182 |
|
183 |
get_sort_order: function() { |
184 |
return parseInt(this.get('metadata') ? this.get('metadata').values.sortorder : -1) |
185 |
}, |
186 |
|
187 |
ssh_keys_path: function() { |
188 |
prepend = '';
|
189 |
if (this.get_created_user() != 'root') { |
190 |
prepend = '/home'
|
191 |
} |
192 |
return '{1}/{0}/.ssh/authorized_keys'.format(this.get_created_user(), prepend); |
193 |
}, |
194 |
|
195 |
_supports_ssh: function() { |
196 |
if (synnefo.config.support_ssh_os_list.indexOf(this.get_os()) > -1) { |
197 |
return true; |
198 |
} |
199 |
return false; |
200 |
}, |
201 |
|
202 |
supports: function(feature) { |
203 |
if (feature == "ssh") { |
204 |
return this._supports_ssh() |
205 |
} |
206 |
return false; |
207 |
}, |
208 |
|
209 |
personality_data_for_keys: function(keys) { |
210 |
contents = '';
|
211 |
_.each(keys, function(key){
|
212 |
contents = contents + key.get("content") + "\n" |
213 |
}); |
214 |
contents = $.base64.encode(contents);
|
215 |
|
216 |
return {
|
217 |
path: this.ssh_keys_path(), |
218 |
contents: contents
|
219 |
} |
220 |
} |
221 |
}); |
222 |
|
223 |
// Flavor model
|
224 |
models.Flavor = models.Model.extend({ |
225 |
path: 'flavors', |
226 |
|
227 |
details_string: function() { |
228 |
return "{0} CPU, {1}MB, {2}GB".format(this.get('cpu'), this.get('ram'), this.get('disk')); |
229 |
}, |
230 |
|
231 |
get_disk_size: function() { |
232 |
return parseInt(this.get("disk") * 1000) |
233 |
} |
234 |
|
235 |
}); |
236 |
|
237 |
//network vms list helper
|
238 |
var NetworkVMSList = function() { |
239 |
this.initialize = function() { |
240 |
this.vms = [];
|
241 |
this.pending = [];
|
242 |
this.pending_for_removal = [];
|
243 |
} |
244 |
|
245 |
this.add_pending_for_remove = function(vm_id) { |
246 |
if (this.pending_for_removal.indexOf(vm_id) == -1) { |
247 |
this.pending_for_removal.push(vm_id);
|
248 |
} |
249 |
|
250 |
if (this.pending_for_removal.length) { |
251 |
this.trigger("pending:remove:add"); |
252 |
} |
253 |
}, |
254 |
|
255 |
this.add_pending = function(vm_id) { |
256 |
if (this.pending.indexOf(vm_id) == -1) { |
257 |
this.pending[this.pending.length] = vm_id; |
258 |
} |
259 |
|
260 |
if (this.pending.length) { |
261 |
this.trigger("pending:add"); |
262 |
} |
263 |
} |
264 |
|
265 |
this.check_pending = function() { |
266 |
var len = this.pending.length; |
267 |
var args = [this.pending]; |
268 |
this.pending = _.difference(this.pending, this.vms); |
269 |
if (len != this.pending.length) { |
270 |
if (this.pending.length == 0) { |
271 |
this.trigger("pending:clear"); |
272 |
} |
273 |
} |
274 |
|
275 |
var len = this.pending_for_removal.length; |
276 |
this.pending_for_removal = _.intersection(this.pending_for_removal, this.vms); |
277 |
if (this.pending_for_removal.length == 0) { |
278 |
this.trigger("pending:remove:clear"); |
279 |
} |
280 |
|
281 |
} |
282 |
|
283 |
|
284 |
this.add = function(vm_id) { |
285 |
if (this.vms.indexOf(vm_id) == -1) { |
286 |
this.vms[this.vms.length] = vm_id; |
287 |
this.trigger("network:connect", vm_id); |
288 |
this.check_pending();
|
289 |
return true; |
290 |
} |
291 |
} |
292 |
|
293 |
this.remove = function(vm_id) { |
294 |
if (this.vms.indexOf(vm_id) > -1) { |
295 |
this.vms = _.without(this.vms, vm_id); |
296 |
this.trigger("network:disconnect", vm_id); |
297 |
this.check_pending();
|
298 |
return true; |
299 |
} |
300 |
} |
301 |
|
302 |
this.get = function() { |
303 |
return this.vms; |
304 |
} |
305 |
|
306 |
this.list = function() { |
307 |
return storage.vms.filter(_.bind(function(vm){ |
308 |
return this.vms.indexOf(vm.id) > -1; |
309 |
}, this))
|
310 |
} |
311 |
|
312 |
this.initialize();
|
313 |
}; |
314 |
_.extend(NetworkVMSList.prototype, bb.Events); |
315 |
|
316 |
// vm networks list helper
|
317 |
var VMNetworksList = function() { |
318 |
this.initialize = function() { |
319 |
this.networks = {};
|
320 |
this.network_ids = [];
|
321 |
} |
322 |
|
323 |
this.add = function(net_id, data) { |
324 |
if (!this.networks[net_id]) { |
325 |
this.networks[net_id] = data || {};
|
326 |
this.network_ids[this.network_ids.length] = net_id; |
327 |
this.trigger("network:connect", net_id); |
328 |
return true; |
329 |
} |
330 |
} |
331 |
|
332 |
this.remove = function(net_id) { |
333 |
if (this.networks[net_id]) { |
334 |
delete this.networks[net_id]; |
335 |
this.network_ids = _.without(this.network_ids, net_id); |
336 |
this.trigger("network:disconnect", net_id); |
337 |
return true; |
338 |
} |
339 |
return false; |
340 |
} |
341 |
|
342 |
this.get = function() { |
343 |
return this.networks; |
344 |
} |
345 |
|
346 |
this.list = function() { |
347 |
return storage.networks.filter(_.bind(function(net){ |
348 |
return this.network_ids.indexOf(net.id) > -1; |
349 |
}, this))
|
350 |
} |
351 |
|
352 |
this.initialize();
|
353 |
}; |
354 |
_.extend(VMNetworksList.prototype, bb.Events); |
355 |
|
356 |
models.ParamsList = function(){this.initialize.apply(this, arguments)}; |
357 |
_.extend(models.ParamsList.prototype, bb.Events, { |
358 |
|
359 |
initialize: function(parent, param_name) { |
360 |
this.parent = parent;
|
361 |
this.actions = {};
|
362 |
this.param_name = param_name;
|
363 |
this.length = 0; |
364 |
}, |
365 |
|
366 |
has_action: function(action) { |
367 |
return this.actions[action] ? true : false; |
368 |
}, |
369 |
|
370 |
_parse_params: function(arguments) { |
371 |
if (arguments.length <= 1) { |
372 |
return [];
|
373 |
} |
374 |
|
375 |
var args = _.toArray(arguments); |
376 |
return args.splice(1); |
377 |
}, |
378 |
|
379 |
contains: function(action, params) { |
380 |
params = this._parse_params(arguments); |
381 |
var has_action = this.has_action(action); |
382 |
if (!has_action) { return false }; |
383 |
|
384 |
var paramsEqual = false; |
385 |
_.each(this.actions[action], function(action_params) { |
386 |
if (_.isEqual(action_params, params)) {
|
387 |
paramsEqual = true;
|
388 |
} |
389 |
}); |
390 |
|
391 |
return paramsEqual;
|
392 |
}, |
393 |
|
394 |
is_empty: function() { |
395 |
return _.isEmpty(this.actions); |
396 |
}, |
397 |
|
398 |
add: function(action, params) { |
399 |
params = this._parse_params(arguments); |
400 |
if (this.contains.apply(this, arguments)) { return this }; |
401 |
var isnew = false |
402 |
if (!this.has_action(action)) { |
403 |
this.actions[action] = [];
|
404 |
isnew = true;
|
405 |
}; |
406 |
|
407 |
this.actions[action].push(params);
|
408 |
this.parent.trigger("change:" + this.param_name, this.parent, this); |
409 |
if (isnew) {
|
410 |
this.trigger("add", action, params); |
411 |
} else {
|
412 |
this.trigger("change", action, params); |
413 |
} |
414 |
return this; |
415 |
}, |
416 |
|
417 |
remove_all: function(action) { |
418 |
if (this.has_action(action)) { |
419 |
delete this.actions[action]; |
420 |
this.parent.trigger("change:" + this.param_name, this.parent, this); |
421 |
this.trigger("remove", action); |
422 |
} |
423 |
return this; |
424 |
}, |
425 |
|
426 |
reset: function() { |
427 |
this.actions = {};
|
428 |
this.parent.trigger("change:" + this.param_name, this.parent, this); |
429 |
this.trigger("reset"); |
430 |
this.trigger("remove"); |
431 |
}, |
432 |
|
433 |
remove: function(action, params) { |
434 |
params = this._parse_params(arguments); |
435 |
if (!this.has_action(action)) { return this }; |
436 |
var index = -1; |
437 |
_.each(this.actions[action], _.bind(function(action_params) { |
438 |
if (_.isEqual(action_params, params)) {
|
439 |
index = this.actions[action].indexOf(action_params);
|
440 |
} |
441 |
}, this));
|
442 |
|
443 |
if (index > -1) { |
444 |
this.actions[action].splice(index, 1); |
445 |
if (_.isEmpty(this.actions[action])) { |
446 |
delete this.actions[action]; |
447 |
} |
448 |
this.parent.trigger("change:" + this.param_name, this.parent, this); |
449 |
this.trigger("remove", action, params); |
450 |
} |
451 |
} |
452 |
|
453 |
}); |
454 |
|
455 |
// Image model
|
456 |
models.Network = models.Model.extend({ |
457 |
path: 'networks', |
458 |
has_status: true, |
459 |
|
460 |
initialize: function() { |
461 |
this.vms = new NetworkVMSList(); |
462 |
this.vms.bind("pending:add", _.bind(this.handle_pending_connections, this, "add")); |
463 |
this.vms.bind("pending:clear", _.bind(this.handle_pending_connections, this, "clear")); |
464 |
this.vms.bind("pending:remove:add", _.bind(this.handle_pending_connections, this, "add")); |
465 |
this.vms.bind("pending:remove:clear", _.bind(this.handle_pending_connections, this, "clear")); |
466 |
|
467 |
var ret = models.Network.__super__.initialize.apply(this, arguments); |
468 |
|
469 |
storage.vms.bind("change:linked_to_nets", _.bind(this.update_connections, this, "vm:change")); |
470 |
storage.vms.bind("add", _.bind(this.update_connections, this, "add")); |
471 |
storage.vms.bind("remove", _.bind(this.update_connections, this, "remove")); |
472 |
storage.vms.bind("reset", _.bind(this.update_connections, this, "reset")); |
473 |
|
474 |
this.bind("change:linked_to", _.bind(this.update_connections, this, "net:change")); |
475 |
this.update_connections();
|
476 |
this.update_state();
|
477 |
|
478 |
this.set({"actions": new models.ParamsList(this, "actions")}); |
479 |
|
480 |
return ret;
|
481 |
}, |
482 |
|
483 |
update_state: function() { |
484 |
if (this.vms.pending.length) { |
485 |
this.set({state: "CONNECTING"}); |
486 |
return
|
487 |
} |
488 |
|
489 |
if (this.vms.pending_for_removal.length) { |
490 |
this.set({state: "DISCONNECTING"}); |
491 |
return
|
492 |
} |
493 |
|
494 |
var firewalling = false; |
495 |
_.each(this.vms.get(), _.bind(function(vm_id){ |
496 |
var vm = storage.vms.get(vm_id);
|
497 |
if (!vm) { return }; |
498 |
if (!_.isEmpty(vm.pending_firewalls)) {
|
499 |
this.set({state:"FIREWALLING"}); |
500 |
firewalling = true;
|
501 |
return false; |
502 |
} |
503 |
},this));
|
504 |
if (firewalling) { return }; |
505 |
|
506 |
this.set({state:"NORMAL"}); |
507 |
}, |
508 |
|
509 |
handle_pending_connections: function(action) { |
510 |
this.update_state();
|
511 |
}, |
512 |
|
513 |
// handle vm/network connections
|
514 |
update_connections: function(action, model) { |
515 |
|
516 |
// vm removed disconnect vm from network
|
517 |
if (action == "remove") { |
518 |
var removed_from_net = this.vms.remove(model.id); |
519 |
var removed_from_vm = model.networks.remove(this.id); |
520 |
if (removed_from_net) {this.trigger("vm:disconnect", model, this); this.change()}; |
521 |
if (removed_from_vm) {model.trigger("network:disconnect", this, model); this.change()}; |
522 |
return;
|
523 |
} |
524 |
|
525 |
// update links for all vms
|
526 |
var links = this.get("linked_to"); |
527 |
storage.vms.each(_.bind(function(vm) {
|
528 |
var vm_links = vm.get("linked_to") || []; |
529 |
if (vm_links.indexOf(this.id) > -1) { |
530 |
// vm has connection to current network
|
531 |
if (links.indexOf(vm.id) > -1) { |
532 |
// and network has connection to vm, so try
|
533 |
// to append it
|
534 |
var add_to_net = this.vms.add(vm.id); |
535 |
var index = _.indexOf(vm_links, this.id); |
536 |
var add_to_vm = vm.networks.add(this.id, vm.get("linked_to_nets")[index]); |
537 |
|
538 |
// call only if connection did not existed
|
539 |
if (add_to_net) {this.trigger("vm:connect", vm, this); this.change()}; |
540 |
if (add_to_vm) {vm.trigger("network:connect", this, vm); vm.change()}; |
541 |
} else {
|
542 |
// no connection, try to remove it
|
543 |
var removed_from_net = this.vms.remove(vm.id); |
544 |
var removed_from_vm = vm.networks.remove(this.id); |
545 |
if (removed_from_net) {this.trigger("vm:disconnect", vm, this); this.change()}; |
546 |
if (removed_from_vm) {vm.trigger("network:disconnect", this, vm); vm.change()}; |
547 |
} |
548 |
} else {
|
549 |
// vm has no connection to current network, try to remove it
|
550 |
var removed_from_net = this.vms.remove(vm.id); |
551 |
var removed_from_vm = vm.networks.remove(this.id); |
552 |
if (removed_from_net) {this.trigger("vm:disconnect", vm, this); this.change()}; |
553 |
if (removed_from_vm) {vm.trigger("network:disconnect", this, vm); vm.change()}; |
554 |
} |
555 |
},this));
|
556 |
}, |
557 |
|
558 |
is_public: function() { |
559 |
return this.id == "public"; |
560 |
}, |
561 |
|
562 |
contains_vm: function(vm) { |
563 |
var net_vm_exists = this.vms.get().indexOf(vm.id) > -1; |
564 |
var vm_net_exists = vm.is_connected_to(this); |
565 |
return net_vm_exists && vm_net_exists;
|
566 |
}, |
567 |
|
568 |
call: function(action, params, success, error) { |
569 |
if (action == "destroy") { |
570 |
this.set({state:"DESTROY"}); |
571 |
this.get("actions").remove("destroy"); |
572 |
this.remove(_.bind(function(){ |
573 |
success(); |
574 |
}, this), error);
|
575 |
} |
576 |
|
577 |
if (action == "disconnect") { |
578 |
_.each(params, _.bind(function(vm_id) {
|
579 |
var vm = snf.storage.vms.get(vm_id);
|
580 |
this.get("actions").remove("disconnect", vm_id); |
581 |
if (vm) {
|
582 |
this.remove_vm(vm, success, error);
|
583 |
} |
584 |
}, this));
|
585 |
} |
586 |
}, |
587 |
|
588 |
add_vm: function (vm, callback, error, options) { |
589 |
var payload = {add:{serverRef:"" + vm.id}}; |
590 |
payload._options = options || {}; |
591 |
return this.api.call(this.api_path() + "/action", "create", |
592 |
payload, |
593 |
_.bind(function(){
|
594 |
this.vms.add_pending(vm.id);
|
595 |
if (callback) {callback()}
|
596 |
},this), error);
|
597 |
}, |
598 |
|
599 |
remove_vm: function (vm, callback, error, options) { |
600 |
var payload = {remove:{serverRef:"" + vm.id}}; |
601 |
payload._options = options || {}; |
602 |
return this.api.call(this.api_path() + "/action", "create", |
603 |
{remove:{serverRef:"" + vm.id}}, |
604 |
_.bind(function(){
|
605 |
this.vms.add_pending_for_remove(vm.id);
|
606 |
if (callback) {callback()}
|
607 |
},this), error);
|
608 |
}, |
609 |
|
610 |
rename: function(name, callback) { |
611 |
return this.api.call(this.api_path(), "update", { |
612 |
network:{name:name}, |
613 |
_options:{
|
614 |
critical: false, |
615 |
error_params:{
|
616 |
title: "Network action failed", |
617 |
ns: "Networks", |
618 |
extra_details: {"Network id": this.id} |
619 |
} |
620 |
}}, callback); |
621 |
}, |
622 |
|
623 |
get_connectable_vms: function() { |
624 |
var servers = this.vms.list(); |
625 |
return storage.vms.filter(function(vm){ |
626 |
return servers.indexOf(vm) == -1 && !vm.in_error_state(); |
627 |
}) |
628 |
}, |
629 |
|
630 |
state_message: function() { |
631 |
if (this.get("state") == "NORMAL" && this.is_public()) { |
632 |
return "Public network"; |
633 |
} |
634 |
|
635 |
return models.Network.STATES[this.get("state")]; |
636 |
}, |
637 |
|
638 |
in_progress: function() { |
639 |
return models.Network.STATES_TRANSITIONS[this.get("state")] != undefined; |
640 |
}, |
641 |
|
642 |
do_all_pending_actions: function(success, error) { |
643 |
var destroy = this.get("actions").has_action("destroy"); |
644 |
_.each(this.get("actions").actions, _.bind(function(params, action) { |
645 |
_.each(params, _.bind(function(with_params) {
|
646 |
this.call(action, with_params, success, error);
|
647 |
}, this));
|
648 |
}, this));
|
649 |
} |
650 |
}); |
651 |
|
652 |
models.Network.STATES = { |
653 |
'NORMAL': 'Private network', |
654 |
'CONNECTING': 'Connecting...', |
655 |
'DISCONNECTING': 'Disconnecting...', |
656 |
'FIREWALLING': 'Firewall update...', |
657 |
'DESTROY': 'Destroying...' |
658 |
} |
659 |
|
660 |
models.Network.STATES_TRANSITIONS = { |
661 |
'CONNECTING': ['NORMAL'], |
662 |
'DISCONNECTING': ['NORMAL'], |
663 |
'FIREWALLING': ['NORMAL'] |
664 |
} |
665 |
|
666 |
// Virtualmachine model
|
667 |
models.VM = models.Model.extend({ |
668 |
|
669 |
path: 'servers', |
670 |
has_status: true, |
671 |
initialize: function(params) { |
672 |
this.networks = new VMNetworksList(); |
673 |
|
674 |
this.pending_firewalls = {};
|
675 |
|
676 |
models.VM.__super__.initialize.apply(this, arguments); |
677 |
|
678 |
this.set({state: params.status || "ERROR"}); |
679 |
this.log = new snf.logging.logger("VM " + this.id); |
680 |
this.pending_action = undefined; |
681 |
|
682 |
// init stats parameter
|
683 |
this.set({'stats': undefined}, {silent: true}); |
684 |
// defaults to not update the stats
|
685 |
// each view should handle this vm attribute
|
686 |
// depending on if it displays stat images or not
|
687 |
this.do_update_stats = false; |
688 |
|
689 |
// interval time
|
690 |
// this will dynamicaly change if the server responds that
|
691 |
// images get refreshed on different intervals
|
692 |
this.stats_update_interval = synnefo.config.STATS_INTERVAL || 5000; |
693 |
this.stats_available = false; |
694 |
|
695 |
// initialize interval
|
696 |
this.init_stats_intervals(this.stats_update_interval); |
697 |
|
698 |
this.bind("change:progress", _.bind(this.update_building_progress, this)); |
699 |
this.update_building_progress();
|
700 |
|
701 |
this.bind("change:firewalls", _.bind(this.handle_firewall_change, this)); |
702 |
|
703 |
// default values
|
704 |
this.set({linked_to_nets:this.get("linked_to_nets") || []}); |
705 |
this.set({firewalls:this.get("firewalls") || []}); |
706 |
|
707 |
this.bind("change:state", _.bind(function(){if (this.state() == "DESTROY") { this.handle_destroy() }}, this)) |
708 |
}, |
709 |
|
710 |
handle_firewall_change: function() { |
711 |
|
712 |
}, |
713 |
|
714 |
set_linked_to_nets: function(data) { |
715 |
this.set({"linked_to":_.map(data, function(n){ return n.id})}); |
716 |
return data;
|
717 |
}, |
718 |
|
719 |
is_connected_to: function(net) { |
720 |
return _.filter(this.networks.list(), function(n){return n.id == net.id}).length > 0; |
721 |
}, |
722 |
|
723 |
status: function(st) { |
724 |
if (!st) { return this.get("status")} |
725 |
return this.set({status:st}); |
726 |
}, |
727 |
|
728 |
set_status: function(st) { |
729 |
var new_state = this.state_for_api_status(st); |
730 |
var transition = false; |
731 |
|
732 |
if (this.state() != new_state) { |
733 |
if (models.VM.STATES_TRANSITIONS[this.state()]) { |
734 |
transition = this.state();
|
735 |
} |
736 |
} |
737 |
|
738 |
// call it silently to avoid double change trigger
|
739 |
this.set({'state': this.state_for_api_status(st)}, {silent: true}); |
740 |
|
741 |
// trigger transition
|
742 |
if (transition && models.VM.TRANSITION_STATES.indexOf(new_state) == -1) { |
743 |
this.trigger("transition", {from:transition, to:new_state}) |
744 |
}; |
745 |
return st;
|
746 |
}, |
747 |
|
748 |
update_building_progress: function() { |
749 |
if (this.is_building()) { |
750 |
var progress = this.get("progress"); |
751 |
if (progress == 0) { |
752 |
this.state("BUILD_INIT"); |
753 |
this.set({progress_message: BUILDING_MESSAGES['INIT']}); |
754 |
} |
755 |
if (progress > 0 && progress < 99) { |
756 |
this.state("BUILD_COPY"); |
757 |
var params = this.get_copy_details(true); |
758 |
this.set({progress_message: BUILDING_MESSAGES['COPY'].format(params.copy, |
759 |
params.size, |
760 |
params.progress)}); |
761 |
} |
762 |
if (progress == 100) { |
763 |
this.state("BUILD_FINAL"); |
764 |
this.set({progress_message: BUILDING_MESSAGES['FINAL']}); |
765 |
} |
766 |
} else {
|
767 |
} |
768 |
}, |
769 |
|
770 |
get_copy_details: function(human, image) { |
771 |
var human = human || false; |
772 |
var image = image || this.get_image(); |
773 |
|
774 |
var progress = this.get('progress'); |
775 |
var size = image.get_size();
|
776 |
var size_copied = (size * progress / 100).toFixed(2); |
777 |
|
778 |
if (human) {
|
779 |
size = util.readablizeBytes(size*1024*1024); |
780 |
size_copied = util.readablizeBytes(size_copied*1024*1024); |
781 |
} |
782 |
return {'progress': progress, 'size': size, 'copy': size_copied}; |
783 |
}, |
784 |
|
785 |
start_stats_update: function(force_if_empty) { |
786 |
var prev_state = this.do_update_stats; |
787 |
|
788 |
this.do_update_stats = true; |
789 |
|
790 |
// fetcher initialized ??
|
791 |
if (!this.stats_fetcher) { |
792 |
this.init_stats_intervals();
|
793 |
} |
794 |
|
795 |
|
796 |
// fetcher running ???
|
797 |
if (!this.stats_fetcher.running || !prev_state) { |
798 |
this.stats_fetcher.start();
|
799 |
} |
800 |
|
801 |
if (force_if_empty && this.get("stats") == undefined) { |
802 |
this.update_stats(true); |
803 |
} |
804 |
}, |
805 |
|
806 |
stop_stats_update: function(stop_calls) { |
807 |
this.do_update_stats = false; |
808 |
|
809 |
if (stop_calls) {
|
810 |
this.stats_fetcher.stop();
|
811 |
} |
812 |
}, |
813 |
|
814 |
// clear and reinitialize update interval
|
815 |
init_stats_intervals: function (interval) { |
816 |
this.stats_fetcher = this.get_stats_fetcher(this.stats_update_interval); |
817 |
this.stats_fetcher.start();
|
818 |
}, |
819 |
|
820 |
get_stats_fetcher: function(timeout) { |
821 |
var cb = _.bind(function(data){ |
822 |
this.update_stats();
|
823 |
}, this);
|
824 |
var fetcher = new snf.api.updateHandler({'callback': cb, timeout:timeout}); |
825 |
return fetcher;
|
826 |
}, |
827 |
|
828 |
// do the api call
|
829 |
update_stats: function(force) { |
830 |
// do not update stats if flag not set
|
831 |
if ((!this.do_update_stats && !force) || this.updating_stats) { |
832 |
return;
|
833 |
} |
834 |
|
835 |
// make the api call, execute handle_stats_update on sucess
|
836 |
// TODO: onError handler ???
|
837 |
stats_url = this.url() + "/stats"; |
838 |
this.updating_stats = true; |
839 |
this.sync("GET", this, { |
840 |
handles_error:true, |
841 |
url: stats_url,
|
842 |
refresh:true, |
843 |
success: _.bind(this.handle_stats_update, this), |
844 |
error: _.bind(this.handle_stats_error, this), |
845 |
complete: _.bind(function(){this.updating_stats = false;}, this), |
846 |
critical: false, |
847 |
display: false, |
848 |
log_error: false |
849 |
}); |
850 |
}, |
851 |
|
852 |
get_stats_image: function(stat, type) { |
853 |
}, |
854 |
|
855 |
_set_stats: function(stats) { |
856 |
var silent = silent === undefined ? false : silent; |
857 |
// unavailable stats while building
|
858 |
if (this.get("status") == "BUILD") { |
859 |
this.stats_available = false; |
860 |
} else { this.stats_available = true; } |
861 |
|
862 |
if (this.get("status") == "DESTROY") { this.stats_available = false; } |
863 |
|
864 |
this.set({stats: stats}, {silent:true}); |
865 |
this.trigger("stats:update", stats); |
866 |
}, |
867 |
|
868 |
unbind: function() { |
869 |
models.VM.__super__.unbind.apply(this, arguments); |
870 |
}, |
871 |
|
872 |
handle_stats_error: function() { |
873 |
stats = {}; |
874 |
_.each(['cpuBar', 'cpuTimeSeries', 'netBar', 'netTimeSeries'], function(k) { |
875 |
stats[k] = false;
|
876 |
}); |
877 |
|
878 |
this.set({'stats': stats}); |
879 |
}, |
880 |
|
881 |
// this method gets executed after a successful vm stats api call
|
882 |
handle_stats_update: function(data) { |
883 |
var self = this; |
884 |
// avoid browser caching
|
885 |
|
886 |
if (data.stats && _.size(data.stats) > 0) { |
887 |
var ts = $.now(); |
888 |
var stats = data.stats;
|
889 |
var images_loaded = 0; |
890 |
var images = {};
|
891 |
|
892 |
function check_images_loaded() { |
893 |
images_loaded++; |
894 |
|
895 |
if (images_loaded == 4) { |
896 |
self._set_stats(images); |
897 |
} |
898 |
} |
899 |
_.each(['cpuBar', 'cpuTimeSeries', 'netBar', 'netTimeSeries'], function(k) { |
900 |
|
901 |
stats[k] = stats[k] + "?_=" + ts;
|
902 |
|
903 |
var stat = k.slice(0,3); |
904 |
var type = k.slice(3,6) == "Bar" ? "bar" : "time"; |
905 |
var img = $("<img />"); |
906 |
var val = stats[k];
|
907 |
|
908 |
// load stat image to a temporary dom element
|
909 |
// update model stats on image load/error events
|
910 |
img.load(function() {
|
911 |
images[k] = val; |
912 |
check_images_loaded(); |
913 |
}); |
914 |
|
915 |
img.error(function() {
|
916 |
images[stat + type] = false;
|
917 |
check_images_loaded(); |
918 |
}); |
919 |
|
920 |
img.attr({'src': stats[k]});
|
921 |
}) |
922 |
data.stats = stats; |
923 |
} |
924 |
|
925 |
// do we need to change the interval ??
|
926 |
if (data.stats.refresh * 1000 != this.stats_update_interval) { |
927 |
this.stats_update_interval = data.stats.refresh * 1000; |
928 |
this.stats_fetcher.timeout = this.stats_update_interval; |
929 |
this.stats_fetcher.stop();
|
930 |
this.stats_fetcher.start(false); |
931 |
} |
932 |
}, |
933 |
|
934 |
// helper method that sets the do_update_stats
|
935 |
// in the future this method could also make an api call
|
936 |
// immediaetly if needed
|
937 |
enable_stats_update: function() { |
938 |
this.do_update_stats = true; |
939 |
}, |
940 |
|
941 |
handle_destroy: function() { |
942 |
this.stats_fetcher.stop();
|
943 |
}, |
944 |
|
945 |
require_reboot: function() { |
946 |
if (this.is_active()) { |
947 |
this.set({'reboot_required': true}); |
948 |
} |
949 |
}, |
950 |
|
951 |
set_pending_action: function(data) { |
952 |
this.pending_action = data;
|
953 |
return data;
|
954 |
}, |
955 |
|
956 |
// machine has pending action
|
957 |
update_pending_action: function(action, force) { |
958 |
this.set({pending_action: action}); |
959 |
}, |
960 |
|
961 |
clear_pending_action: function() { |
962 |
this.set({pending_action: undefined}); |
963 |
}, |
964 |
|
965 |
has_pending_action: function() { |
966 |
return this.get("pending_action") ? this.get("pending_action") : false; |
967 |
}, |
968 |
|
969 |
// machine is active
|
970 |
is_active: function() { |
971 |
return models.VM.ACTIVE_STATES.indexOf(this.state()) > -1; |
972 |
}, |
973 |
|
974 |
// machine is building
|
975 |
is_building: function() { |
976 |
return models.VM.BUILDING_STATES.indexOf(this.state()) > -1; |
977 |
}, |
978 |
|
979 |
in_error_state: function() { |
980 |
return this.state() === "ERROR" |
981 |
}, |
982 |
|
983 |
// user can connect to machine
|
984 |
is_connectable: function() { |
985 |
// check if ips exist
|
986 |
if (!this.get_addresses().ip4 && !this.get_addresses().ip6) { |
987 |
return false; |
988 |
} |
989 |
return models.VM.CONNECT_STATES.indexOf(this.state()) > -1; |
990 |
}, |
991 |
|
992 |
set_firewalls: function(data) { |
993 |
_.each(data, _.bind(function(val, key){
|
994 |
if (this.pending_firewalls && this.pending_firewalls[key] && this.pending_firewalls[key] == val) { |
995 |
this.require_reboot();
|
996 |
this.remove_pending_firewall(key, val);
|
997 |
} |
998 |
}, this));
|
999 |
return data;
|
1000 |
}, |
1001 |
|
1002 |
remove_pending_firewall: function(net_id, value) { |
1003 |
if (this.pending_firewalls[net_id] == value) { |
1004 |
delete this.pending_firewalls[net_id]; |
1005 |
storage.networks.get(net_id).update_state(); |
1006 |
} |
1007 |
}, |
1008 |
|
1009 |
remove_meta: function(key, complete, error) { |
1010 |
var url = this.api_path() + "/meta/" + key; |
1011 |
this.api.call(url, "delete", undefined, complete, error); |
1012 |
}, |
1013 |
|
1014 |
save_meta: function(meta, complete, error) { |
1015 |
var url = this.api_path() + "/meta/" + meta.key; |
1016 |
var payload = {meta:{}}; |
1017 |
payload.meta[meta.key] = meta.value; |
1018 |
payload._options = { |
1019 |
critical:false, |
1020 |
error_params: {
|
1021 |
title: "Machine metadata error", |
1022 |
extra_details: {"Machine id": this.id} |
1023 |
}}; |
1024 |
|
1025 |
this.api.call(url, "update", payload, complete, error); |
1026 |
}, |
1027 |
|
1028 |
set_firewall: function(net_id, value, callback, error, options) { |
1029 |
if (this.get("firewalls") && this.get("firewalls")[net_id] == value) { return } |
1030 |
|
1031 |
this.pending_firewalls[net_id] = value;
|
1032 |
this.trigger("change", this, this); |
1033 |
var payload = {"firewallProfile":{"profile":value}}; |
1034 |
payload._options = _.extend({critical: false}, options); |
1035 |
|
1036 |
// reset firewall state on error
|
1037 |
var error_cb = _.bind(function() { |
1038 |
thi |
1039 |
}, this);
|
1040 |
|
1041 |
this.api.call(this.api_path() + "/action", "create", payload, callback, error); |
1042 |
storage.networks.get(net_id).update_state(); |
1043 |
}, |
1044 |
|
1045 |
firewall_pending: function(net_id) { |
1046 |
return this.pending_firewalls[net_id] != undefined; |
1047 |
}, |
1048 |
|
1049 |
// update/get the state of the machine
|
1050 |
state: function() { |
1051 |
var args = slice.call(arguments); |
1052 |
|
1053 |
// TODO: it might not be a good idea to set the state in set_state method
|
1054 |
if (args.length > 0 && models.VM.STATES.indexOf(args[0]) > -1) { |
1055 |
this.set({'state': args[0]}); |
1056 |
} |
1057 |
|
1058 |
return this.get('state'); |
1059 |
}, |
1060 |
|
1061 |
// get the state that the api status corresponds to
|
1062 |
state_for_api_status: function(status) { |
1063 |
return this.state_transition(this.state(), status); |
1064 |
}, |
1065 |
|
1066 |
// vm state equals vm api status
|
1067 |
state_is_status: function(state) { |
1068 |
return models.VM.STATUSES.indexOf(state) != -1; |
1069 |
}, |
1070 |
|
1071 |
// get transition state for the corresponging api status
|
1072 |
state_transition: function(state, new_status) { |
1073 |
var statuses = models.VM.STATES_TRANSITIONS[state];
|
1074 |
if (statuses) {
|
1075 |
if (statuses.indexOf(new_status) > -1) { |
1076 |
return new_status;
|
1077 |
} else {
|
1078 |
return state;
|
1079 |
} |
1080 |
} else {
|
1081 |
return new_status;
|
1082 |
} |
1083 |
}, |
1084 |
|
1085 |
// the current vm state is a transition state
|
1086 |
in_transition: function() { |
1087 |
return models.VM.TRANSITION_STATES.indexOf(this.state()) > -1 || |
1088 |
models.VM.TRANSITION_STATES.indexOf(this.get('status')) > -1; |
1089 |
}, |
1090 |
|
1091 |
// get image object
|
1092 |
// TODO: update images synchronously if image not found
|
1093 |
get_image: function() { |
1094 |
var image = storage.images.get(this.get('imageRef')); |
1095 |
if (!image) {
|
1096 |
storage.images.update_unknown_id(this.get('imageRef')); |
1097 |
image = storage.images.get(this.get('imageRef')); |
1098 |
} |
1099 |
return image;
|
1100 |
}, |
1101 |
|
1102 |
// get flavor object
|
1103 |
get_flavor: function() { |
1104 |
var flv = storage.flavors.get(this.get('flavorRef')); |
1105 |
if (!flv) {
|
1106 |
storage.flavors.update_unknown_id(this.get('flavorRef')); |
1107 |
flv = storage.flavors.get(this.get('flavorRef')); |
1108 |
} |
1109 |
return flv;
|
1110 |
}, |
1111 |
|
1112 |
// retrieve the metadata object
|
1113 |
get_meta: function() { |
1114 |
try {
|
1115 |
return this.get('metadata').values |
1116 |
} catch (err) {
|
1117 |
return {};
|
1118 |
} |
1119 |
}, |
1120 |
|
1121 |
// get metadata OS value
|
1122 |
get_os: function() { |
1123 |
return this.get_meta().OS || (this.get_image() ? this.get_image().get_os() || "okeanos" : "okeanos"); |
1124 |
}, |
1125 |
|
1126 |
// get public ip addresses
|
1127 |
// TODO: public network is always the 0 index ???
|
1128 |
get_addresses: function(net_id) { |
1129 |
var net_id = net_id || "public"; |
1130 |
|
1131 |
var info = this.get_network_info(net_id); |
1132 |
if (!info) { return {} }; |
1133 |
addrs = {}; |
1134 |
_.each(info.values, function(addr) {
|
1135 |
addrs["ip" + addr.version] = addr.addr;
|
1136 |
}); |
1137 |
return addrs
|
1138 |
}, |
1139 |
|
1140 |
get_network_info: function(net_id) { |
1141 |
var net_id = net_id || "public"; |
1142 |
|
1143 |
if (!this.networks.network_ids.length) { return {} }; |
1144 |
|
1145 |
var addresses = this.networks.get(); |
1146 |
try {
|
1147 |
return _.select(addresses, function(net, key){return key == net_id })[0]; |
1148 |
} catch (err) {
|
1149 |
//this.log.debug("Cannot find network {0}".format(net_id))
|
1150 |
} |
1151 |
}, |
1152 |
|
1153 |
firewall_profile: function(net_id) { |
1154 |
var net_id = net_id || "public"; |
1155 |
var firewalls = this.get("firewalls"); |
1156 |
return firewalls[net_id];
|
1157 |
}, |
1158 |
|
1159 |
has_firewall: function(net_id) { |
1160 |
var net_id = net_id || "public"; |
1161 |
return ["ENABLED","PROTECTED"].indexOf(this.firewall_profile()) > -1; |
1162 |
}, |
1163 |
|
1164 |
// get actions that the user can execute
|
1165 |
// depending on the vm state/status
|
1166 |
get_available_actions: function() { |
1167 |
return models.VM.AVAILABLE_ACTIONS[this.state()]; |
1168 |
}, |
1169 |
|
1170 |
set_profile: function(profile, net_id) { |
1171 |
}, |
1172 |
|
1173 |
// call rename api
|
1174 |
rename: function(new_name) { |
1175 |
//this.set({'name': new_name});
|
1176 |
this.sync("update", this, { |
1177 |
critical: true, |
1178 |
data: {
|
1179 |
'server': {
|
1180 |
'name': new_name
|
1181 |
} |
1182 |
}, |
1183 |
// do the rename after the method succeeds
|
1184 |
success: _.bind(function(){ |
1185 |
//this.set({name: new_name});
|
1186 |
snf.api.trigger("call");
|
1187 |
}, this)
|
1188 |
}); |
1189 |
}, |
1190 |
|
1191 |
get_console_url: function(data) { |
1192 |
var url_params = {
|
1193 |
machine: this.get("name"), |
1194 |
host_ip: this.get_addresses().ip4, |
1195 |
host_ip_v6: this.get_addresses().ip6, |
1196 |
host: data.host,
|
1197 |
port: data.port,
|
1198 |
password: data.password
|
1199 |
} |
1200 |
return '/machines/console?' + $.param(url_params); |
1201 |
}, |
1202 |
|
1203 |
// action helper
|
1204 |
call: function(action_name, success, error) { |
1205 |
var id_param = [this.id]; |
1206 |
|
1207 |
success = success || function() {};
|
1208 |
error = error || function() {};
|
1209 |
|
1210 |
var self = this; |
1211 |
|
1212 |
switch(action_name) {
|
1213 |
case 'start': |
1214 |
this.__make_api_call(this.get_action_url(), // vm actions url |
1215 |
"create", // create so that sync later uses POST to make the call |
1216 |
{start:{}}, // payload |
1217 |
function() {
|
1218 |
// set state after successful call
|
1219 |
self.state("START");
|
1220 |
success.apply(this, arguments); |
1221 |
snf.api.trigger("call");
|
1222 |
}, |
1223 |
error, 'start');
|
1224 |
break;
|
1225 |
case 'reboot': |
1226 |
this.__make_api_call(this.get_action_url(), // vm actions url |
1227 |
"create", // create so that sync later uses POST to make the call |
1228 |
{reboot:{type:"HARD"}}, // payload |
1229 |
function() {
|
1230 |
// set state after successful call
|
1231 |
self.state("REBOOT");
|
1232 |
success.apply(this, arguments) |
1233 |
snf.api.trigger("call");
|
1234 |
self.set({'reboot_required': false}); |
1235 |
}, |
1236 |
error, 'reboot');
|
1237 |
break;
|
1238 |
case 'shutdown': |
1239 |
this.__make_api_call(this.get_action_url(), // vm actions url |
1240 |
"create", // create so that sync later uses POST to make the call |
1241 |
{shutdown:{}}, // payload |
1242 |
function() {
|
1243 |
// set state after successful call
|
1244 |
self.state("SHUTDOWN");
|
1245 |
success.apply(this, arguments) |
1246 |
snf.api.trigger("call");
|
1247 |
}, |
1248 |
error, 'shutdown');
|
1249 |
break;
|
1250 |
case 'console': |
1251 |
this.__make_api_call(this.url() + "/action", "create", {'console': {'type':'vnc'}}, function(data) { |
1252 |
var cons_data = data.console;
|
1253 |
success.apply(this, [cons_data]);
|
1254 |
}, undefined, 'console') |
1255 |
break;
|
1256 |
case 'destroy': |
1257 |
this.__make_api_call(this.url(), // vm actions url |
1258 |
"delete", // create so that sync later uses POST to make the call |
1259 |
undefined, // payload |
1260 |
function() {
|
1261 |
// set state after successful call
|
1262 |
self.state('DESTROY');
|
1263 |
success.apply(this, arguments) |
1264 |
}, |
1265 |
error, 'destroy');
|
1266 |
break;
|
1267 |
default:
|
1268 |
throw "Invalid VM action ("+action_name+")"; |
1269 |
} |
1270 |
}, |
1271 |
|
1272 |
__make_api_call: function(url, method, data, success, error, action) { |
1273 |
var self = this; |
1274 |
error = error || function(){};
|
1275 |
success = success || function(){};
|
1276 |
|
1277 |
var params = {
|
1278 |
url: url,
|
1279 |
data: data,
|
1280 |
success: function(){ self.handle_action_succeed.apply(self, arguments); success.apply(this, arguments)}, |
1281 |
error: function(){ self.handle_action_fail.apply(self, arguments); error.apply(this, arguments)}, |
1282 |
error_params: { ns: "Machines actions", |
1283 |
title: "'" + this.get("name") + "'" + " " + action + " failed", |
1284 |
extra_details: { 'Machine ID': this.id, 'URL': url, 'Action': action || "undefined" }, |
1285 |
allow_reload: false |
1286 |
}, |
1287 |
display: false, |
1288 |
critical: false |
1289 |
} |
1290 |
this.sync(method, this, params); |
1291 |
}, |
1292 |
|
1293 |
handle_action_succeed: function() { |
1294 |
this.trigger("action:success", arguments); |
1295 |
}, |
1296 |
|
1297 |
reset_action_error: function() { |
1298 |
this.action_error = false; |
1299 |
this.trigger("action:fail:reset", this.action_error); |
1300 |
}, |
1301 |
|
1302 |
handle_action_fail: function() { |
1303 |
this.action_error = arguments; |
1304 |
this.trigger("action:fail", arguments); |
1305 |
}, |
1306 |
|
1307 |
get_action_url: function(name) { |
1308 |
return this.url() + "/action"; |
1309 |
}, |
1310 |
|
1311 |
get_connection_info: function(host_os, success, error) { |
1312 |
var url = "/machines/connect"; |
1313 |
params = { |
1314 |
ip_address: this.get_addresses().ip4, |
1315 |
os: this.get_os(), |
1316 |
host_os: host_os,
|
1317 |
srv: this.id |
1318 |
} |
1319 |
|
1320 |
url = url + "?" + $.param(params); |
1321 |
|
1322 |
var ajax = snf.api.sync("read", undefined, { url: url, |
1323 |
error:error,
|
1324 |
success:success,
|
1325 |
handles_error:1}); |
1326 |
} |
1327 |
}) |
1328 |
|
1329 |
models.VM.ACTIONS = [ |
1330 |
'start',
|
1331 |
'shutdown',
|
1332 |
'reboot',
|
1333 |
'console',
|
1334 |
'destroy'
|
1335 |
] |
1336 |
|
1337 |
models.VM.AVAILABLE_ACTIONS = { |
1338 |
'UNKNWON' : ['destroy'], |
1339 |
'BUILD' : ['destroy'], |
1340 |
'REBOOT' : ['shutdown', 'destroy', 'console'], |
1341 |
'STOPPED' : ['start', 'destroy'], |
1342 |
'ACTIVE' : ['shutdown', 'destroy', 'reboot', 'console'], |
1343 |
'ERROR' : ['destroy'], |
1344 |
'DELETED' : [],
|
1345 |
'DESTROY' : [],
|
1346 |
'BUILD_INIT' : ['destroy'], |
1347 |
'BUILD_COPY' : ['destroy'], |
1348 |
'BUILD_FINAL' : ['destroy'], |
1349 |
'SHUTDOWN' : ['destroy'], |
1350 |
'START' : [],
|
1351 |
'CONNECT' : [],
|
1352 |
'DISCONNECT' : []
|
1353 |
} |
1354 |
|
1355 |
// api status values
|
1356 |
models.VM.STATUSES = [ |
1357 |
'UNKNWON',
|
1358 |
'BUILD',
|
1359 |
'REBOOT',
|
1360 |
'STOPPED',
|
1361 |
'ACTIVE',
|
1362 |
'ERROR',
|
1363 |
'DELETED'
|
1364 |
] |
1365 |
|
1366 |
// api status values
|
1367 |
models.VM.CONNECT_STATES = [ |
1368 |
'ACTIVE',
|
1369 |
'REBOOT',
|
1370 |
'SHUTDOWN'
|
1371 |
] |
1372 |
|
1373 |
// vm states
|
1374 |
models.VM.STATES = models.VM.STATUSES.concat([ |
1375 |
'DESTROY',
|
1376 |
'BUILD_INIT',
|
1377 |
'BUILD_COPY',
|
1378 |
'BUILD_FINAL',
|
1379 |
'SHUTDOWN',
|
1380 |
'START',
|
1381 |
'CONNECT',
|
1382 |
'DISCONNECT',
|
1383 |
'FIREWALL'
|
1384 |
]); |
1385 |
|
1386 |
models.VM.STATES_TRANSITIONS = { |
1387 |
'DESTROY' : ['DELETED'], |
1388 |
'SHUTDOWN': ['ERROR', 'STOPPED', 'DESTROY'], |
1389 |
'STOPPED': ['ERROR', 'ACTIVE', 'DESTROY'], |
1390 |
'ACTIVE': ['ERROR', 'STOPPED', 'REBOOT', 'SHUTDOWN', 'DESTROY'], |
1391 |
'START': ['ERROR', 'ACTIVE', 'DESTROY'], |
1392 |
'REBOOT': ['ERROR', 'ACTIVE', 'STOPPED', 'DESTROY'], |
1393 |
'BUILD': ['ERROR', 'ACTIVE', 'DESTROY'], |
1394 |
'BUILD_COPY': ['ERROR', 'ACTIVE', 'BUILD_FINAL', 'DESTROY'], |
1395 |
'BUILD_FINAL': ['ERROR', 'ACTIVE', 'DESTROY'], |
1396 |
'BUILD_INIT': ['ERROR', 'ACTIVE', 'BUILD_COPY', 'BUILD_FINAL', 'DESTROY'] |
1397 |
} |
1398 |
|
1399 |
models.VM.TRANSITION_STATES = [ |
1400 |
'DESTROY',
|
1401 |
'SHUTDOWN',
|
1402 |
'START',
|
1403 |
'REBOOT',
|
1404 |
'BUILD'
|
1405 |
] |
1406 |
|
1407 |
models.VM.ACTIVE_STATES = [ |
1408 |
'BUILD', 'REBOOT', 'ACTIVE', |
1409 |
'BUILD_INIT', 'BUILD_COPY', 'BUILD_FINAL', |
1410 |
'SHUTDOWN', 'CONNECT', 'DISCONNECT' |
1411 |
] |
1412 |
|
1413 |
models.VM.BUILDING_STATES = [ |
1414 |
'BUILD', 'BUILD_INIT', 'BUILD_COPY', 'BUILD_FINAL' |
1415 |
] |
1416 |
|
1417 |
models.Networks = models.Collection.extend({ |
1418 |
model: models.Network,
|
1419 |
path: 'networks', |
1420 |
details: true, |
1421 |
//noUpdate: true,
|
1422 |
defaults: {'linked_to':[]}, |
1423 |
|
1424 |
parse: function (resp, xhr) { |
1425 |
// FIXME: depricated global var
|
1426 |
if (!resp) { return []}; |
1427 |
|
1428 |
var data = _.map(resp.networks.values, _.bind(this.parse_net_api_data, this)); |
1429 |
return data;
|
1430 |
}, |
1431 |
|
1432 |
reset_pending_actions: function() { |
1433 |
this.each(function(net) { |
1434 |
net.get("actions").reset();
|
1435 |
}) |
1436 |
}, |
1437 |
|
1438 |
do_all_pending_actions: function() { |
1439 |
this.each(function(net) { |
1440 |
net.do_all_pending_actions(); |
1441 |
}) |
1442 |
}, |
1443 |
|
1444 |
parse_net_api_data: function(data) { |
1445 |
if (data.servers && data.servers.values) {
|
1446 |
data['linked_to'] = data.servers.values;
|
1447 |
} |
1448 |
return data;
|
1449 |
}, |
1450 |
|
1451 |
create: function (name, callback) { |
1452 |
return this.api.call(this.path, "create", {network:{name:name}}, callback); |
1453 |
} |
1454 |
}) |
1455 |
|
1456 |
models.Images = models.Collection.extend({ |
1457 |
model: models.Image,
|
1458 |
path: 'images', |
1459 |
details: true, |
1460 |
noUpdate: true, |
1461 |
supportIncUpdates: false, |
1462 |
meta_keys_as_attrs: ["OS", "description", "kernel", "size", "GUI"], |
1463 |
|
1464 |
// update collection model with id passed
|
1465 |
// making a direct call to the image
|
1466 |
// api url
|
1467 |
update_unknown_id: function(id) { |
1468 |
var url = getUrl.call(this) + "/" + id; |
1469 |
this.api.call(this.path + "/" + id, "read", {_options:{async:false}}, undefined, |
1470 |
_.bind(function() {
|
1471 |
this.add({id:id, name:"Unknown image", size:-1, progress:100, status:"DELETED"}) |
1472 |
}, this), _.bind(function(image) { |
1473 |
this.add(image.image);
|
1474 |
}, this));
|
1475 |
}, |
1476 |
|
1477 |
parse: function (resp, xhr) { |
1478 |
// FIXME: depricated global var
|
1479 |
var data = _.map(resp.images.values, _.bind(this.parse_meta, this)); |
1480 |
return resp.images.values;
|
1481 |
}, |
1482 |
|
1483 |
get_meta_key: function(img, key) { |
1484 |
if (img.metadata && img.metadata.values && img.metadata.values[key]) {
|
1485 |
return img.metadata.values[key];
|
1486 |
} |
1487 |
return undefined; |
1488 |
}, |
1489 |
|
1490 |
comparator: function(img) { |
1491 |
return -img.get_sort_order("sortorder") || 1000 * img.id; |
1492 |
}, |
1493 |
|
1494 |
parse_meta: function(img) { |
1495 |
_.each(this.meta_keys_as_attrs, _.bind(function(key){ |
1496 |
img[key] = this.get_meta_key(img, key) || ""; |
1497 |
}, this));
|
1498 |
return img;
|
1499 |
}, |
1500 |
|
1501 |
active: function() { |
1502 |
return this.filter(function(img){return img.get('status') != "DELETED"}); |
1503 |
} |
1504 |
}) |
1505 |
|
1506 |
models.Flavors = models.Collection.extend({ |
1507 |
model: models.Flavor,
|
1508 |
path: 'flavors', |
1509 |
details: true, |
1510 |
noUpdate: true, |
1511 |
supportIncUpdates: false, |
1512 |
// update collection model with id passed
|
1513 |
// making a direct call to the flavor
|
1514 |
// api url
|
1515 |
update_unknown_id: function(id) { |
1516 |
var url = getUrl.call(this) + "/" + id; |
1517 |
this.api.call(this.path + "/" + id, "read", {_options:{async:false}}, undefined, |
1518 |
_.bind(function() {
|
1519 |
this.add({id:id, cpu:"", ram:"", disk:"", name: "", status:"DELETED"}) |
1520 |
}, this), _.bind(function(flv) { |
1521 |
if (!flv.flavor.status) { flv.flavor.status = "DELETED" }; |
1522 |
this.add(flv.flavor);
|
1523 |
}, this));
|
1524 |
}, |
1525 |
|
1526 |
parse: function (resp, xhr) { |
1527 |
// FIXME: depricated global var
|
1528 |
return resp.flavors.values;
|
1529 |
}, |
1530 |
|
1531 |
comparator: function(flv) { |
1532 |
return flv.get("disk") * flv.get("cpu") * flv.get("ram"); |
1533 |
}, |
1534 |
|
1535 |
unavailable_values_for_image: function(img, flavors) { |
1536 |
var flavors = flavors || this.active(); |
1537 |
var size = img.get_size();
|
1538 |
|
1539 |
var index = {cpu:[], disk:[], ram:[]}; |
1540 |
|
1541 |
_.each(this.active(), function(el) { |
1542 |
var img_size = size;
|
1543 |
var flv_size = el.get_disk_size();
|
1544 |
if (flv_size < img_size) {
|
1545 |
if (index.disk.indexOf(flv_size) == -1) { |
1546 |
index.disk.push(flv_size); |
1547 |
} |
1548 |
}; |
1549 |
}); |
1550 |
|
1551 |
return index;
|
1552 |
}, |
1553 |
|
1554 |
get_flavor: function(cpu, mem, disk, filter_list) { |
1555 |
if (!filter_list) { filter_list = this.models }; |
1556 |
|
1557 |
return this.select(function(flv){ |
1558 |
if (flv.get("cpu") == cpu + "" && |
1559 |
flv.get("ram") == mem + "" && |
1560 |
flv.get("disk") == disk + "" && |
1561 |
filter_list.indexOf(flv) > -1) { return true; } |
1562 |
})[0];
|
1563 |
}, |
1564 |
|
1565 |
get_data: function(lst) { |
1566 |
var data = {'cpu': [], 'mem':[], 'disk':[]}; |
1567 |
|
1568 |
_.each(lst, function(flv) {
|
1569 |
if (data.cpu.indexOf(flv.get("cpu")) == -1) { |
1570 |
data.cpu.push(flv.get("cpu"));
|
1571 |
} |
1572 |
if (data.mem.indexOf(flv.get("ram")) == -1) { |
1573 |
data.mem.push(flv.get("ram"));
|
1574 |
} |
1575 |
if (data.disk.indexOf(flv.get("disk")) == -1) { |
1576 |
data.disk.push(flv.get("disk"));
|
1577 |
} |
1578 |
}) |
1579 |
|
1580 |
return data;
|
1581 |
}, |
1582 |
|
1583 |
active: function() { |
1584 |
return this.filter(function(flv){return flv.get('status') != "DELETED"}); |
1585 |
} |
1586 |
|
1587 |
}) |
1588 |
|
1589 |
models.VMS = models.Collection.extend({ |
1590 |
model: models.VM,
|
1591 |
path: 'servers', |
1592 |
details: true, |
1593 |
copy_image_meta: true, |
1594 |
|
1595 |
parse: function (resp, xhr) { |
1596 |
// FIXME: depricated after refactoring
|
1597 |
var data = resp;
|
1598 |
if (!resp) { return [] }; |
1599 |
data = _.filter(_.map(resp.servers.values, _.bind(this.parse_vm_api_data, this)), function(v){return v}); |
1600 |
return data;
|
1601 |
}, |
1602 |
|
1603 |
get_reboot_required: function() { |
1604 |
return this.filter(function(vm){return vm.get("reboot_required") == true}) |
1605 |
}, |
1606 |
|
1607 |
has_pending_actions: function() { |
1608 |
return this.filter(function(vm){return vm.pending_action}).length > 0; |
1609 |
}, |
1610 |
|
1611 |
reset_pending_actions: function() { |
1612 |
this.each(function(vm) { |
1613 |
vm.clear_pending_action(); |
1614 |
}) |
1615 |
}, |
1616 |
|
1617 |
do_all_pending_actions: function(success, error) { |
1618 |
this.each(function(vm) { |
1619 |
if (vm.has_pending_action()) {
|
1620 |
vm.call(vm.pending_action, success, error); |
1621 |
vm.clear_pending_action(); |
1622 |
} |
1623 |
}) |
1624 |
}, |
1625 |
|
1626 |
do_all_reboots: function(success, error) { |
1627 |
this.each(function(vm) { |
1628 |
if (vm.get("reboot_required")) { |
1629 |
vm.call("reboot", success, error);
|
1630 |
} |
1631 |
}); |
1632 |
}, |
1633 |
|
1634 |
reset_reboot_required: function() { |
1635 |
this.each(function(vm) { |
1636 |
vm.set({'reboot_required': undefined}); |
1637 |
}) |
1638 |
}, |
1639 |
|
1640 |
stop_stats_update: function(exclude) { |
1641 |
var exclude = exclude || [];
|
1642 |
this.each(function(vm) { |
1643 |
if (exclude.indexOf(vm) > -1) { |
1644 |
return;
|
1645 |
} |
1646 |
vm.stop_stats_update(); |
1647 |
}) |
1648 |
}, |
1649 |
|
1650 |
has_meta: function(vm_data) { |
1651 |
return vm_data.metadata && vm_data.metadata.values
|
1652 |
}, |
1653 |
|
1654 |
has_addresses: function(vm_data) { |
1655 |
return vm_data.metadata && vm_data.metadata.values
|
1656 |
}, |
1657 |
|
1658 |
parse_vm_api_data: function(data) { |
1659 |
// do not add non existing DELETED entries
|
1660 |
if (data.status && data.status == "DELETED") { |
1661 |
if (!this.get(data.id)) { |
1662 |
console.error("non exising deleted vm", data)
|
1663 |
return false; |
1664 |
} |
1665 |
} |
1666 |
|
1667 |
// OS attribute
|
1668 |
if (this.has_meta(data)) { |
1669 |
data['OS'] = data.metadata.values.OS || "okeanos"; |
1670 |
} |
1671 |
|
1672 |
data['firewalls'] = {};
|
1673 |
if (data['addresses'] && data['addresses'].values) { |
1674 |
data['linked_to_nets'] = data['addresses'].values; |
1675 |
_.each(data['addresses'].values, function(f){ |
1676 |
if (f['firewallProfile']) { |
1677 |
data['firewalls'][f['id']] = f['firewallProfile'] |
1678 |
} |
1679 |
}); |
1680 |
} |
1681 |
|
1682 |
// if vm has no metadata, no metadata object
|
1683 |
// is in json response, reset it to force
|
1684 |
// value update
|
1685 |
if (!data['metadata']) { |
1686 |
data['metadata'] = {values:{}}; |
1687 |
} |
1688 |
|
1689 |
return data;
|
1690 |
}, |
1691 |
|
1692 |
create: function (name, image, flavor, meta, extra, callback) { |
1693 |
if (this.copy_image_meta) { |
1694 |
meta['OS'] = image.get("OS"); |
1695 |
} |
1696 |
|
1697 |
opts = {name: name, imageRef: image.id, flavorRef: flavor.id, metadata:meta} |
1698 |
opts = _.extend(opts, extra); |
1699 |
|
1700 |
this.api.call(this.path, "create", {'server': opts}, undefined, undefined, callback, {critical: false}); |
1701 |
} |
1702 |
|
1703 |
}) |
1704 |
|
1705 |
models.PublicKey = models.Model.extend({ |
1706 |
path: 'keys', |
1707 |
base_url: '/ui/userdata', |
1708 |
details: false, |
1709 |
noUpdate: true, |
1710 |
|
1711 |
|
1712 |
get_public_key: function() { |
1713 |
return cryptico.publicKeyFromString(this.get("content")); |
1714 |
}, |
1715 |
|
1716 |
get_filename: function() { |
1717 |
return "{0}.pub".format(this.get("name")); |
1718 |
}, |
1719 |
|
1720 |
identify_type: function() { |
1721 |
try {
|
1722 |
var cont = snf.util.validatePublicKey(this.get("content")); |
1723 |
var type = cont.split(" ")[0]; |
1724 |
return synnefo.util.publicKeyTypesMap[type];
|
1725 |
} catch (err) { return false }; |
1726 |
} |
1727 |
|
1728 |
}) |
1729 |
|
1730 |
models.PublicKeys = models.Collection.extend({ |
1731 |
model: models.PublicKey,
|
1732 |
details: false, |
1733 |
path: 'keys', |
1734 |
base_url: '/ui/userdata', |
1735 |
noUpdate: true, |
1736 |
|
1737 |
comparator: function(i) { return -parseInt(i.id || 0) }, |
1738 |
|
1739 |
generate_new: function(passphrase, length, save) { |
1740 |
|
1741 |
var passphrase = passphrase || ""; |
1742 |
var length = length || 1024; |
1743 |
var key = cryptico.generateRSAKey(passphrase, length);
|
1744 |
|
1745 |
var b64enc = $.base64.encode; |
1746 |
return key;
|
1747 |
}, |
1748 |
|
1749 |
add_crypto_key: function(key, success, error, options) { |
1750 |
var options = options || {};
|
1751 |
var m = new models.PublicKey(); |
1752 |
var name_tpl = "public key"; |
1753 |
var name = name_tpl;
|
1754 |
var name_count = 1; |
1755 |
|
1756 |
while(this.filter(function(m){ return m.get("name") == name }).length > 0) { |
1757 |
name = name_tpl + " " + name_count;
|
1758 |
name_count++; |
1759 |
} |
1760 |
|
1761 |
m.set({name: name});
|
1762 |
m.set({content: "ssh-rsa AAAAB3NzaC1yc2EA" + cryptico.publicKeyString(key)}); |
1763 |
|
1764 |
options.success = function () { return success(m) }; |
1765 |
options.errror = error; |
1766 |
|
1767 |
this.create(m.attributes, options);
|
1768 |
} |
1769 |
}) |
1770 |
|
1771 |
// storage initialization
|
1772 |
snf.storage.images = new models.Images();
|
1773 |
snf.storage.flavors = new models.Flavors();
|
1774 |
snf.storage.networks = new models.Networks();
|
1775 |
snf.storage.vms = new models.VMS();
|
1776 |
snf.storage.keys = new models.PublicKeys();
|
1777 |
|
1778 |
//snf.storage.vms.fetch({update:true});
|
1779 |
//snf.storage.images.fetch({update:true});
|
1780 |
//snf.storage.flavors.fetch({update:true});
|
1781 |
|
1782 |
})(this);
|