From 104ed24675158c52d179c5c9a852571afd57293a Mon Sep 17 00:00:00 2001 From: Marco Ziccardi Date: Mon, 13 Jul 2015 19:51:51 +0200 Subject: [PATCH 1/9] First commit support to Google Compute Engine instances, disks and firewall rules --- lib/compute/disk.js | 147 +++++++ lib/compute/firewall.js | 259 ++++++++++++ lib/compute/index.js | 872 ++++++++++++++++++++++++++++++++++++++++ lib/compute/instance.js | 333 +++++++++++++++ lib/index.js | 12 + 5 files changed, 1623 insertions(+) create mode 100644 lib/compute/disk.js create mode 100644 lib/compute/firewall.js create mode 100644 lib/compute/index.js create mode 100644 lib/compute/instance.js diff --git a/lib/compute/disk.js b/lib/compute/disk.js new file mode 100644 index 00000000000..c5916735c72 --- /dev/null +++ b/lib/compute/disk.js @@ -0,0 +1,147 @@ +/*! + * Copyright 2014 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*! + * @module compute/disk + */ + +'use strict'; + +/** + * @type {module:common/util} + * @private + */ +var util = require('../common/util.js'); + +/** + * @const {string} + * @private + */ +var COMPUTE_BASE_URL = 'https://www.googleapis.com/compute/v1/projects/'; + +/** + * Create a Disk object to interact with a Google Compute Engine disk. + * + * @constructor + * @alias module:compute/disk + * + * @throws {Error} if a disk name or a zone are not provided. + * + * @param {module:compute} compute - The Google Compute Engine instance this + * disk belongs to. + * @param {string} name - Name of the disk. + * @param {string} zone - Zone to which the disk belongs. + * @param {number=} sizeGb - Size of the disk in GB. + * @param {object=} metadata - Disk metadata. + * + * @example + * var gcloud = require('gcloud'); + * + * var compute = gcloud.compute({ + * projectId: 'grape-spaceship-123' + * }); + * + * var disk = compute.disk('disk-1', 'europe-west1-b'); + */ +function Disk(compute, name, zone, sizeGb, metadata) { + this.name = name; + this.zone = zone; + + if (!this.name) { + throw new Error('A name is needed to use Google Cloud Compute Disk.'); + } + if (!this.zone) { + throw new Error('A zone is needed to use Google Cloud Compute Disk.'); + } + + this.sizeGb = sizeGb; + this.compute = compute; + this.metadata = metadata; +} + +/** + * Delete the disk. + * + * @param {function} callback - The callback function. + * + * @example + * disk.delete(function(err) {}); + */ +Disk.prototype.delete = function(callback) { + callback = callback || util.noop; + this.makeReq_('DELETE', '', null, true, callback); +}; + +/** + * Create a snapshot of a disk. + * + * @param {string} name - Name of the snapshot. + * @param {string=} description - Description of the snapshot. + * @param {function} callback - The callback function. + * + * @example + * disk.createSnapshot('my-snapshot', function(err, apiResponse) { + * // Handle error + * }); + */ +Disk.prototype.createSnapshot = function(name, description, callback) { + if (!name) { + throw new Error('A name is required to create a snapshot.'); + } else if (!/^(?:[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?)$/.test(name)) { + throw new Error('Name must match [a-z]([-a-z0-9]{0,61}[a-z0-9])?'); + } + if (!callback) { + callback = description; + } + + var body = {}; + body.name = name; + if (description) { + body.description = description; + } + + this.makeReq_('POST', '/createSnapshot', null, body, callback); +}; + +/** + * Make a new request object from the provided arguments and wrap the callback + * to intercept non-successful responses. + * + * @private + * + * @param {string} method - Action. + * @param {string} path - Request path. + * @param {*} query - Request query object. + * @param {*} body - Request body contents. + * @param {function} callback - The callback function. + */ +Disk.prototype.makeReq_ = function(method, path, query, body, callback) { + var reqOpts = { + method: method, + qs: query, + uri: COMPUTE_BASE_URL + this.compute.projectId + + '/zones/' + this.zone + + '/disks/' + this.name + path + }; + + if (body) { + reqOpts.json = body; + } + + this.compute.makeAuthorizedRequest_(reqOpts, callback); +}; + +module.exports = Disk; \ No newline at end of file diff --git a/lib/compute/firewall.js b/lib/compute/firewall.js new file mode 100644 index 00000000000..0865e3bf125 --- /dev/null +++ b/lib/compute/firewall.js @@ -0,0 +1,259 @@ +/*! + * Copyright 2014 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*! + * @module compute/firewall + */ + +'use strict'; + +/** + * @type {module:common/util} + * @private + */ +var util = require('../common/util.js'); + +/** + * @const {string} + * @private + */ +var COMPUTE_BASE_URL = 'https://www.googleapis.com/compute/v1/projects/'; + +/** + * Create a Firewall object to interact with a Google Compute Engine firewall. + * + * @constructor + * @alias module:compute/firewall + * + * @throws {Error} if a firewall name isn't provided. + * + * @param {module:compute} compute - The Google Compute Engine instance this + * firewall belongs to. + * @param {string} name - Name of the firewall. + * @param {object} options - Firewall options. + * @param {string=} options.description - Description of the firewall. + * @param {string=} options.network - Network to which the firewall applies. + * Default value is 'global/networks/default'. + * @param {object[]=} options.allowed - List of allowed protocols and ports. + * @param {string[]=} options.sourceRanges - IP address blocks to which this + * rule applies (in CIDR format). + * @param {string[]=} options.sourceRanges - IP address blocks to which this + * rule applies (in CIDR format). + * @param {string[]=} options.sourceTags - List of instance tags to which + * this rule applies. + * @param {string[]=} options.targetTags - List of instance tags indicating + * instances that may process connections according to this rule. + * + * @example + * var gcloud = require('gcloud'); + * + * var compute = gcloud.compute({ + * projectId: 'grape-spaceship-123' + * }); + * + * var firewall = compute.firewall('tcp-3000'); + */ +function Firewall(compute, name, options) { + this.name = name; + this.compute = compute; + this.description = ''; + this.network = 'global/networks/default'; + this.allowed = []; + this.sourceRanges = []; + this.sourceTags = []; + this.targetTags = []; + + if (!this.name) { + throw new Error('A name is needed to use Google Cloud Compute Firewall.'); + } + + if (options) { + if (util.is(options.description, 'string')) { + this.description = options.description; + } + if (util.is(options.network, 'string')) { + this.network = options.network; + } + if (util.is(options.allowed, 'array')) { + this.allowed = options.allowed; + } + if (util.is(options.sourceRanges, 'array')) { + this.sourceRanges = options.sourceRanges; + } + if (util.is(options.sourceTags, 'array')) { + this.sourceTags = options.sourceTags; + } + if (util.is(options.targetTags, 'array')) { + this.targetTags = options.targetTags; + } + } +} + +/** + * Delete the firewall rule. + * + * @param {function} callback - The callback function. + * + * @example + * firewall.delete(function(err) {}); + */ +Firewall.prototype.delete = function(callback) { + callback = callback || util.noop; + this.makeReq_('DELETE', '', null, true, callback); +}; + +/** + * Update the firewall rule. + * + * @throws {Error} if firewall options are not provided, if no allowed + * protocols are specified or none of source ranges and source tags are + * provided + * + * @param {module:compute} compute - The Google Compute Engine instance this + * this firewall belongs to. + * @param {string} name - Name of the firewall. + * @param {object} options - Firewall options. + * @param {boolean} options.patch - If true update is performed according to + * the patch semantics (default is false). + * @param {string=} options.description - Description of the firewall. + * @param {string=} options.network - Network to which the firewall applies. + * Default value is 'global/networks/default'. + * @param {object[]=} options.allowed - List of allowed protocols and ports. + * @param {string[]=} options.sourceRanges - IP address blocks to which this + * rule applies (in CIDR format). + * @param {string[]=} options.sourceRanges - IP address blocks to which this + * rule applies (in CIDR format). + * @param {string[]=} options.sourceTags - List of instance tags to which + * this rule applies. + * @param {string[]=} options.targetTags - List of instance tags indicating + * instances that may process connections according to this rule. + * @param {function} callback - The callback function. + * + * @example + * var callback = function(err, firewall, apiResponse) { + * // `firewall` is a Firewall object. + * }; + * + * firewall.update( + * { + * patch: true, + * description: 'Brand new description', + * }, callback); + */ +Firewall.prototype.update = function(options, callback) { + if (!options) { + throw new Error('Firewall rule options must be provided.'); + } + + var method = 'PUT'; + if (options.patch) { + method = 'PATCH'; + } else { + if(!util.is(options.allowed, 'array')) { + throw new Error('Allowed protocols and ports must be provided.'); + } + if (!util.is(options.sourceRanges, 'array') && + !util.is(options.sourceTags, 'array')) { + throw new Error('Source ranges or source tags must be provided.'); + } + } + + var body = {}; + if (util.is(options.name, 'string')) { + body.name = options.name; + } else { + body.name = this.name; + } + var newFirewall = this.compute.firewall(body.name); + if (util.is(options.description, 'string')) { + body.description = options.description; + newFirewall.description = body.description; + } else { + newFirewall.description = this.description; + } + if (util.is(options.network, 'string')) { + body.network = options.network; + newFirewall.network = body.network; + } else { + body.network = this.network; + newFirewall.network = this.network; + } + if (util.is(options.allowed, 'array')) { + body.allowed = options.allowed; + newFirewall.allowed = body.allowed; + } else { + newFirewall.allowed = this.allowed; + } + if (util.is(options.sourceRanges, 'array')) { + body.sourceRanges = options.sourceRanges; + newFirewall.sourceRanges = body.sourceRanges; + } else { + newFirewall.sourceRanges = this.sourceRanges; + } + if (util.is(options.sourceTags, 'array')) { + body.sourceTags = options.sourceTags; + newFirewall.sourceTags = body.sourceTags; + } else { + newFirewall.sourceTags = this.sourceTags; + } + if (util.is(options.targetTags, 'array')) { + body.targetTags = options.targetTags; + newFirewall.targetTags = body.targetTags; + } else { + newFirewall.targetTags = this.targetTags; + } + + this.makeReq_( + method, + '', + null, + body, function(err, resp) { + if (err) { + callback(err); + return; + } + callback(null, newFirewall, resp); + }.bind(this)); +}; + +/** + * Make a new request object from the provided arguments and wrap the callback + * to intercept non-successful responses. + * + * @private + * + * @param {string} method - Action. + * @param {string} path - Request path. + * @param {*} query - Request query object. + * @param {*} body - Request body contents. + * @param {function} callback - The callback function. + */ +Firewall.prototype.makeReq_ = function(method, path, query, body, callback) { + var reqOpts = { + method: method, + qs: query, + uri: COMPUTE_BASE_URL + this.compute.projectId + + '/global/firewalls/' + this.name + }; + + if (body) { + reqOpts.json = body; + } + + this.compute.makeAuthorizedRequest_(reqOpts, callback); +}; + +module.exports = Firewall; \ No newline at end of file diff --git a/lib/compute/index.js b/lib/compute/index.js new file mode 100644 index 00000000000..27db32fb5be --- /dev/null +++ b/lib/compute/index.js @@ -0,0 +1,872 @@ +/*! + * Copyright 2014 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*! + * @module compute + */ + +'use strict'; + +/** + * @type {module:compute/instance} + * @private + */ +var Instance = require('./instance.js'); + +/** + * @type {module:compute/disk} + * @private + */ +var Disk = require('./disk.js'); + +/** + * @type {module:compute/firewall} + * @private + */ +var Firewall = require('./firewall.js'); + +/** + * @type {module:common/util} + * @private + */ +var util = require('../common/util.js'); + +/** + * Required scopes for Google Compute Engine API. + * @const {array} + * @private + */ +var SCOPES = ['https://www.googleapis.com/auth/compute']; + +/** + * @const {string} + * @private + */ +var COMPUTE_BASE_URL = 'https://www.googleapis.com/compute/v1/projects/'; + +/** + * Create a Compute object to Interact with the Google Compute Engine API. + * Using this object, you can access your instances with + * {module:compute/instance}, disks with {module:compute/disk} and firewall + * rules with {module:compute/firewall}. + * + * Follow along with the examples to see how to do everything. With a Compute + * object it is possible to search for existing instances, disks and firewall + * as well as to create new ones. Instances can be stopped, reset and started + * and their tags can be updated. Disks can be deleted and attached to running + * instances. Firewall rules can be created and updated. + * + * @alias module:compute + * @constructor + * + * @param {object} options - [Configuration object](#/docs/?method=gcloud). + * + * @example + * var gcloud = require('gcloud')({ + * keyFilename: '/path/to/keyfile.json', + * projectId: 'grape-spaceship-123' + * }); + * + * var compute = gcloud.compute(); + */ +function Compute(options) { + if (!(this instanceof Compute)) { + return new Compute(options); + } + + options = options || {}; + + this.makeAuthorizedRequest_ = util.makeAuthorizedRequestFactory({ + credentials: options.credentials, + keyFile: options.keyFilename, + scopes: SCOPES, + email: options.email + }); + + this.projectId = options.projectId; +} + +/** + * Get a reference to a Google Compute Engine instance. + * + * @param {string} name - Name of the existing instance. + * @param {string} zone - Zone of the existing instance. + * @return {module:compute/instance} + * + * @example + * var gcloud = require('gcloud')({ + * keyFilename: '/path/to/keyfile.json' + * }); + * + * var compute = gcloud.compute(); + * + * var instance = compute.instance('instance1', 'europe-west1-b'); + */ +Compute.prototype.instance = function(name, zone) { + return new Instance(this, name, zone); +}; + +/** + * Get a reference to a Google Compute Engine disk. + * + * @param {string} name - Name of the existing disk. + * @param {string} zone - Zone of the existing disk. + * @return {module:compute/disk} + * + * @example + * var gcloud = require('gcloud')({ + * keyFilename: '/path/to/keyfile.json' + * }); + * + * var compute = gcloud.compute(); + * + * var disk = compute.disk('disk1', 'europe-west1-b'); + */ +Compute.prototype.disk = function(name, zone) { + return new Disk(this, name, zone); +}; + +/** + * Get a reference to a Google Compute Engine firewall. + * + * @param {string} name - Name of the existing firewall. + * @param {string=} networkName - Network name for the existing firewall. + * Default value is 'global/networks/default'. + * @return {module:compute/disk} + * + * @example + * var gcloud = require('gcloud')({ + * keyFilename: '/path/to/keyfile.json' + * }); + * + * var compute = gcloud.compute(); + * + * var firewallRule = compute.firewall('rule1'); + */ +Compute.prototype.firewall = function(name, networkName) { + if (networkName) { + return new Firewall(this, networkName, { + network: networkName + }); + } + return new Firewall(this, name); +}; + +/** + * Create an instance. + * + * @throws {Error} if an instance name or options are not provided. + * + * @param {string} name - Name of the instance. + * @param {object} options - Options for instance creation. + * @param {string} options.zone - Zone where the instance is created. + * @param {string} machineType - Type of the instance. + * @param {object[]} disks - Disks to attach to the instance. + * @param {boolean} disks[].createNew - True if the disk has to be created. + * Default value is false. + * @param {string} disks[].source - Source for the disk to be created, either an + * image or a snapshot. + * @param {string=} disks[].diskType - Type of the disk. + * @param {number=} disks[].sizeGb - Size of the disk in GB. + * @param {boolean=} disks[].boot - True of the disk is a boot disk. + * @param {string=} disks[].mode - Attach mode, either `READ_ONLY` or + * `READ_WRITE`. Default value is `READ_WRITE`. + * @param {number=} disks[].index - Zero based index assigned from the instance + * to the disk. + * @param {string=} disks[].deviceName - Device name assigned from the instance + * to the disk. + * @param {boolean=} disks[].autoDelete - If true the disk is deleted when the + * instance is deleted. + * @param {object[]} networkInterfaces - Network interfaces for the instance. + * @param {string} networkInterfaces[].network - Full/partial URL of a network + * interface for this instance. + * @param {function} callback - The callback function. + * + * @example + * var callback = function(err, instance, apiResponse) { + * // `instance` is an Instance object. + * }; + * + * var compute = gcloud.compute({ + * projectId: 'grape-spaceship-123' + * }); + * + * compute.createInstance( + * 'instance-1', + * { + * machineType: 'zones/europe-west1-b/machineTypes/n1-standard-1', + * zone: 'europe-west1-b', + * disks: [{ + * createNew: true, + * diskName: 'test-create-disk', + * source: + * 'projects/debian-cloud/global/images/debian-7-wheezy-v20150325', + * diskSizeGb: 10, + * boot: true, + * autoDelete: true + * }], + * networkInterfaces: [{ + * network: 'global/networks/default' + * }] + * }, callback); + */ +Compute.prototype.createInstance = function(name, options, callback) { + if (!name) { + throw new Error('A name is required to create an instance.'); + } else if (!/^(?:[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?)$/.test(name)) { + throw new Error('Name must match [a-z]([-a-z0-9]{0,61}[a-z0-9])?'); + } + if (!options) { + throw new Error('Options are required to create an instance.'); + } + if (!options.zone) { + throw new Error('A zone is required to create an instance.'); + } + if (!options.machineType) { + throw new Error('A machine type is required to create an instance.'); + } + if (!callback) { + throw new Error('A callback must be defined.'); + } + + var query = {}; + + var body = { + name: name, + machineType: options.machineType, + disks: [], + networkInterfaces: [] + }; + + if (!util.is(options.disks, 'array')) { + options.disks = [options.disks]; + } + + options.disks.forEach(function(disk) { + var requestDisk = {}; + if (util.is(disk.source, 'string')) { + if (disk.createNew) { + requestDisk.initializeParams = { + sourceImage: disk.source + }; + } else { + requestDisk.source = disk.source; + } + } else { + throw new Error('A disk source must be provided.'); + } + if (util.is(disk.diskSizeGb, 'long')) { + requestDisk.initializeParams.diskSizeGb = disk.diskSizeGb; + } + if (util.is(disk.diskType, 'string')) { + requestDisk.initializeParams.diskType = disk.diskType; + } + if (util.is(disk.diskName, 'string')) { + requestDisk.initializeParams.diskName = disk.diskName; + } + if (util.is(disk.type, 'string')) { + requestDisk.type = disk.type; + } + if (util.is(disk.mode, 'string')) { + requestDisk.mode = disk.mode; + } + if (util.is(disk.index, 'integer')) { + requestDisk.index = disk.index; + } + if (util.is(disk.deviceName, 'string')) { + requestDisk.deviceName = disk.deviceName; + } + if (util.is(disk.boot, 'boolean')) { + requestDisk.boot = disk.boot; + } + if (util.is(disk.autoDelete, 'boolean')) { + requestDisk.autoDelete = disk.autoDelete; + } + body.disks.push(requestDisk); + }); + if (body.disks.lenght === 0) { + throw new Error('A disk must be provided to create an instance.'); + } + if (!util.is(options.networkIntefaces, 'array')) { + options.networkIntefaces = [options.networks]; + } + options.networkInterfaces.forEach(function(networkInterface) { + body.networkInterfaces.push({ + network: networkInterface.network + }); + }); + + this.makeReq_( + 'POST', + '/zones/'+options.zone+'/instances', + query, + body, function(err, resp) { + if (err) { + callback(err); + return; + } + var instance = new Instance(this, name, options.zone); + callback(null, instance, resp); + }.bind(this)); +}; + +/** + * Create a disk. + * + * @throws {Error} if a disk name or options are not provided or if both a + * source image and a source snapshot are provided. + * + * @param {string} name - Name of the disk. + * @param {object} options - Options for disk creation. + * @param {string} options.zone - Zone where the disk is created. + * @param {string=} sourceImage - Source image for the disk. + * @param {string=} sourceSnapshot - Source snapshot for the disk. + * @param {number=} sizeGb - Size of the disk in GB. + * @param {function} callback - The callback function. + * + * @example + * var callback = function(err, disk, apiResponse) { + * // `disk` is a Disk object. + * }; + * + * var compute = gcloud.compute({ + * projectId: 'grape-spaceship-123' + * }); + * + * compute.createDisk( + * 'new-disk', + * { + * zone: 'europe-west1-b', + * sizeGb: 10 + * }, callback); + */ +Compute.prototype.createDisk = function(name, options, callback) { + if (!name) { + throw new Error('A name is required to create a disk.'); + } else if (!/^(?:[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?)$/.test(name)) { + throw new Error('Name must match [a-z]([-a-z0-9]{0,61}[a-z0-9])?'); + } + if (!options) { + throw new Error('Options are required to create a disk.'); + } + if (!options.zone) { + throw new Error('A zone is required to create a disk.'); + } + if (!options.sourceImage && !options.sizeGb && !options.sourceSnapshot) { + throw new Error('An image, a snapshot or the size is required.'); + } + if (options.sourceImage && options.sourceSnapshot) { + throw new Error('An image and a snapshot can not be both provided.'); + } + if (!callback) { + throw new Error('A callback must be defined.'); + } + + var query = {}; + var body = {}; + + if (options.sourceImage) { + query.sourceImage = options.sourceImage; + } else if (options.sourceSnapshot) { + body.sourceSnapshot = options.sourceSnapshot; + } + + if (util.is(options.sizeGb, 'number')) { + body.sizeGb = options.sizeGb.toString(); + } + + body.name = name; + + this.makeReq_( + 'POST', + '/zones/'+options.zone+'/disks', + query, + body, function(err, resp) { + if (err) { + callback(err); + return; + } + var disk = new Disk(this, name, options.zone, options.sizeGb); + callback(null, disk, resp); + }.bind(this)); +}; + +/** + * Create a firewall rule. + * + * @throws {Error} if a firewall name or firewall options are not provided. + * If allowed ports, source tags or source ranges are not provided. + * + * @param {module:compute} compute - The Google Compute Engine instance this + * this firewall belongs to. + * @param {string} name - Name of the firewall. + * @param {object} options - Firewall options. + * @param {string=} options.description - Description of the firewall. + * @param {string=} options.network - Network to which the firewall applies. + * Default value is 'global/networks/default'. + * @param {object[]=} options.allowed - List of allowed protocols and ports. + * @param {string[]=} options.sourceRanges - IP address blocks to which this + * rule applies (in CIDR format). + * @param {string[]=} options.sourceRanges - IP address blocks to which this + * rule applies (in CIDR format). + * @param {string[]=} options.sourceTags - List of instance tags to which + * this rule applies. + * @param {string[]=} options.targetTags - List of instance tags indicating + * instances that may process connections according to this rule. + * @param {function} callback - The callback function. + * + * @example + * var callback = function(err, firewall, apiResponse) { + * // `firewall` is a Firewall object. + * }; + * + * var compute = gcloud.compute({ + * projectId: 'grape-spaceship-123' + * }); + * + * var firewall = compute.createFirewall('tcp-3000', + * { + * description: 'yada yada', + * allowed: [{ + * IPProtocol: 'tcp', + * ports: ['3000'] + * }], + * sourceRanges: ['0.0.0.0/0'], + * targetTags: ['tcp-3000-tag'] + * }, callback); + */ +Compute.prototype.createFirewall = function(name, options, callback) { + if (!name) { + throw new Error('A firewall name is needed to use Google Cloud Compute.'); + } else if (!/^(?:[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?)$/.test(name)) { + throw new Error('Name must match [a-z]([-a-z0-9]{0,61}[a-z0-9])?'); + } + if (!options || !util.is(options.allowed, 'array')) { + throw new Error('Allowed protocols and ports must be provided.'); + } + if (!util.is(options.sourceRanges, 'array') && + !util.is(options.sourceTags, 'array')) { + throw new Error('Source ranges or source tags must be provided.'); + } + + var body = {}; + body.name = name; + if (util.is(options.description, 'string')) { + body.description = options.description; + } + if (util.is(options.network, 'string')) { + body.network = options.network; + } else { + body.network = 'global/networks/default'; + } + if (util.is(options.allowed, 'array')) { + body.allowed = options.allowed; + } + if (util.is(options.sourceRanges, 'array')) { + body.sourceRanges = options.sourceRanges; + } + if (util.is(options.sourceTags, 'array')) { + body.sourceTags = options.sourceTags; + } + if (util.is(options.targetTags, 'array')) { + body.targetTags = options.targetTags; + } + + this.makeReq_( + 'POST', + '/global/firewalls', + null, + body, function(err, resp) { + if (err) { + callback(err); + return; + } + var firewall = new Firewall(this, name, body); + callback(null, firewall, resp); + }.bind(this)); +}; + +/** + * Get a list of instances. + * + * @throws {Error} if a malformed filter is provided. + * + * @param {object} options - Instance search options. + * @param {number=} options.maxResults - Maximum number of instances to + * return. + * @param {string=} options.zone - Only instances in this zone a returned. + * @param {object=} options.filter - Search filter. + * @param {string} options.filter.fieldName - Instance field to consider in this + * filter. + * @param {string} options.filter.operatorString - Filter operator, can be + * either 'eq' or 'ne'. + * @param {string} options.filter.literalString - String to compare the instance + * field to. Can be a regular expression. + * @param {string=} pageToken - Page identifier used in paginated search. + * @param {function} callback - The callback function. + * + * @example + * var callback = function(err, instances) { + * // `instances` is an array of `Instance` objects. + * }; + * + * var compute = gcloud.compute({ + * projectId: 'grape-spaceship-123' + * }); + * + * compute.listInstances( + * { + * zone: 'europe-west1-b', + * filter: { + * fieldName : 'name', + * operatorString : 'eq', + * literalString : 'instance-[0-9]' + * }], + * }, callback); + */ +Compute.prototype.listInstances = function(options, callback) { + if (!callback) { + callback = options; + } + + var query = {}; + + if (util.is(options.maxResults, 'number')) { + query.maxResults = options.maxResults; + } + if (options.filter) { + if (!util.is(options.filter.fieldName, 'string') || + !util.is(options.filter.operatorString, 'string') || + !util.is(options.filter.literalString, 'string')) { + throw new Error( + 'A filter must have name, comparisonString and literal fields.'); + } + if (options.filter.operatorString !== 'eq' && + options.filter.operatorString !== 'ne') { + throw new Error('Filter operator must be either eq or ne'); + } + query.filter = + options.filter.fieldName + ' ' + + options.filter.operatorString + ' \'' + + options.filter.literalString + '\''; + } + if (util.is(options.pageToken, 'string')) { + query.pageToken = options.pageToken; + } + + var path = ''; + var processResponse; + if (options.zone) { + path = '/zones/'+options.zone; + processResponse = function(resp) { + var instances = []; + for (var i = 0; i < resp.items.length; i++) { + var instance = new Instance( + this, + resp.items[i].name, + resp.items[i].zone, + resp.items[i]); + instances.push(instance); + } + callback(null, instances); + }.bind(this); + } else { + path = '/aggregated'; + processResponse = function(resp) { + var instances = []; + for (var zone in resp.items) { + var zoneInstances = resp.items[zone].instances; + if (zoneInstances) { + for (var i = 0; i < zoneInstances.length; i++) { + var instance = new Instance( + this, + zoneInstances[i].name, + zoneInstances[i].zone, + zoneInstances[i]); + instances.push(instance); + } + } + } + callback(null, instances); + }.bind(this); + } + + this.makeReq_( + 'GET', + path+'/instances', + query, + null, function(err, resp) { + if (err) { + callback(err); + return; + } + if (resp.items) { + processResponse(resp); + } else { + callback(null, []); + } + }); +}; + +/** + * Get a list of disks. + * + * @throws {Error} if a malformed filter is provided. + * + * @param {object} options - Disk search options. + * @param {number=} options.maxResults - Maximum number of disks to return. + * @param {string=} options.zone - Only disks in this zone a returned. + * @param {object=} options.filter - Search filter. + * @param {string} options.filter.fieldName - Disk field to consider in this + * filter. + * @param {string} options.filter.operatorString - Filter operator, can be + * either 'eq' or 'ne'. + * @param {string} options.filter.literalString - String to compare the disk + * field to. Can be a regular expression. + * @param {string=} pageToken - Page identifier used in paginated search. + * @param {function} callback - The callback function. + * + * @example + * var callback = function(err, disks) { + * // `disks` is an array of `Disk` objects. + * }; + * + * var compute = gcloud.compute({ + * projectId: 'grape-spaceship-123' + * }); + * + * compute.listDisks( + * { + * zone: 'europe-west1-b', + * filter: { + * fieldName : 'name', + * operatorString : 'eq', + * literalString : 'disk-[0-9]' + * }], + * }, callback); + */ +Compute.prototype.listDisks = function(options, callback) { + if (!callback) { + callback = options; + } + + var query = {}; + + if (util.is(options.maxResults, 'number')) { + query.maxResults = options.maxResults; + } + if (options.filter) { + if (!util.is(options.filter.fieldName, 'string') || + !util.is(options.filter.operatorString, 'string') || + !util.is(options.filter.literalString, 'string')) { + throw new Error( + 'A filter must have name, comparisonString and literal fields.'); + } + if (options.filter.operatorString !== 'eq' && + options.filter.operatorString !== 'ne') { + throw new Error('Filter operator must be either eq or ne'); + } + query.filter = + options.filter.fieldName + ' ' + + options.filter.operatorString + ' \'' + + options.filter.literalString + '\''; + } + if (util.is(options.pageToken, 'string')) { + query.pageToken = options.pageToken; + } + + var path = ''; + var processResponse; + if (options.zone) { + path = '/zones/'+options.zone; + processResponse = function(resp) { + var disks = []; + for (var i = 0; i < resp.items.length; i++) { + var disk = new Disk( + this, + resp.items[i].name, + resp.items[i].zone, + resp.items[i].sizeGb, + resp.items[i]); + disks.push(disk); + } + callback(null, disks); + }.bind(this); + } else { + path = '/aggregated'; + processResponse = function(resp) { + var disks = []; + for (var zone in resp.items) { + var zoneDisks = resp.items[zone].disks; + if (zoneDisks) { + for (var i = 0; i < zoneDisks.length; i++) { + var disk = new Disk( + this, + zoneDisks[i].name, + zoneDisks[i].zone, + zoneDisks[i].sizeGb, + zoneDisks[i]); + disks.push(disk); + } + } + } + callback(null, disks); + }.bind(this); + } + + this.makeReq_( + 'GET', + path+'/disks', + query, + null, function(err, resp) { + if (err) { + callback(err); + return; + } + if (resp.items) { + processResponse(resp); + } else { + callback(null, []); + } + }); +}; + +/** + * Get a list of firewall rules. + * + * @throws {Error} if a malformed filter is provided. + * + * @param {object} options - Firewall search options. + * @param {number=} options.maxResults - Maximum number of firewalls to return. + * @param {object=} options.filter - Search filter. + * @param {string} options.filter.fieldName - Firewall field to consider in this + * filter. + * @param {string} options.filter.operatorString - Filter operator, can be + * either 'eq' or 'ne'. + * @param {string} options.filter.literalString - String to compare the firewall + * field to. Can be a regular expression. + * @param {string=} pageToken - Page identifier used in paginated search. + * @param {function} callback - The callback function. + * + * @example + * var callback = function(err, firewalls) { + * // `firewalls` is an array of `Firewall` objects. + * }; + * + * var compute = gcloud.compute({ + * projectId: 'grape-spaceship-123' + * }); + * + * compute.listFirewalls( + * { + * zone: 'europe-west1-b', + * filter: { + * fieldName : 'name', + * operatorString : 'eq', + * literalString : 'firewall-[0-9]' + * }], + * }, callback); + */ +Compute.prototype.listFirewalls = function(options, callback) { + if (!callback) { + callback = options; + } + + var query = {}; + + if (util.is(options.maxResults, 'number')) { + query.maxResults = options.maxResults; + } + if (options.filter) { + if (!util.is(options.filter.fieldName, 'string') || + !util.is(options.filter.operatorString, 'string') || + !util.is(options.filter.literalString, 'string')) { + throw new Error( + 'A filter must have name, comparisonString and literal fields.'); + } + if (options.filter.operatorString !== 'eq' && + options.filter.operatorString !== 'ne') { + throw new Error('Filter operator must be either eq or ne'); + } + query.filter = + options.filter.fieldName + ' ' + + options.filter.operatorString + ' \'' + + options.filter.literalString + '\''; + } + if (util.is(options.pageToken, 'string')) { + query.pageToken = options.pageToken; + } + + this.makeReq_( + 'GET', + '/global/firewalls', + query, + null, function(err, resp) { + if (err) { + callback(err); + return; + } + if (resp.items) { + var firewalls = []; + for (var i = 0; i < resp.items.length; i++) { + var firewall = new Firewall( + this, + resp.items[i].name, + { + description: resp.items[i].description, + network: resp.items[i].network, + allowed: resp.items[i].allowed, + sourceRanges: resp.items[i].sourceRanges, + sourceTags: resp.items[i].sourceTags, + targetTags: resp.items[i].targetTags + }); + firewalls.push(firewall); + } + callback(null, firewalls); + } else { + callback(null, []); + } + }.bind(this)); +}; + +/** + * Make a new request object from the provided arguments and wrap the callback + * to intercept non-successful responses. + * + * @private + * + * @param {string} method - Action. + * @param {string} path - Request path. + * @param {*} query - Request query object. + * @param {*} body - Request body contents. + * @param {function} callback - The callback function. + */ +Compute.prototype.makeReq_ = function(method, path, query, body, callback) { + var reqOpts = { + method: method, + qs: query, + uri: COMPUTE_BASE_URL + this.projectId + path + }; + + if (body) { + reqOpts.json = body; + } + + this.makeAuthorizedRequest_(reqOpts, callback); +}; + +module.exports = Compute; + diff --git a/lib/compute/instance.js b/lib/compute/instance.js new file mode 100644 index 00000000000..d55390eea04 --- /dev/null +++ b/lib/compute/instance.js @@ -0,0 +1,333 @@ +/*! + * Copyright 2014 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*! + * @module compute/instance + */ + +'use strict'; + +/** + * @type {module:common/util} + * @private + */ +var util = require('../common/util.js'); + +/** + * @const {string} + * @private + */ +var COMPUTE_BASE_URL = 'https://www.googleapis.com/compute/v1/projects/'; + +/** + * Create a Instance object to interact with a Google Compute Engine disk. + * + * @constructor + * @alias module:compute/istance + * + * @throws {Error} if a disk name or a zone are not provided. + * + * @param {module:compute} compute - The Google Compute Engine instance this + * instance belongs to. + * @param {string} name - Name of the instance. + * @param {string} zone - Zone to which the instance belongs. + * @param {object=} metadata - Instance metadata. + * + * @example + * var gcloud = require('gcloud'); + * + * var compute = gcloud.compute({ + * projectId: 'grape-spaceship-123' + * }); + * + * var instance = compute.instance('instance-1', 'europe-west1-b'); + */ +function Instance(compute, name, zone, metadata) { + this.name = name; + this.zone = zone; + this.compute = compute; + this.metadata = metadata; + + if (!this.name) { + throw Error('An instance name is needed to create a Compute Instance.'); + } +} + +/** + * Delete the instance. + * + * @param {function} callback - The callback function. + * + * @example + * instance.delete(function(err) {}); + */ +Instance.prototype.delete = function(callback) { + callback = callback || util.noop; + this.makeReq_('DELETE', '', null, true, callback); +}; + +/** + * Attach a disk to the instance. + * + * @throws {Error} if no disk is provided. + * + * @param {module:compute/disk} disk - The disk to attach. + * @param {object=} options - Disk attach options. + * @param {boolean=} otpions.boot - If true the disk is attached as a boot disk. + * @param {boolean=} options.readOnly - If true the disk is attached as + * `READ_ONLY`. + * @param {number=} options.index - Zero based index assigned from the instance + * to the disk. + * @param {string=} options.deviceName - Device name assigned from the instance + * to the disk. + * @param {boolean=} options.autoDelete - If true the disk is deleted when the + * instance is deleted. + * @param {function} callback - The callback function. + * + * @example + * instance.attachDisk( + * disk, + * { + * readOnly : true + * } + * function(err, apiResponse) {}); + */ +Instance.prototype.attachDisk = function(disk, options, callback) { + if (!disk) { + throw new Error('A disk must be provided.'); + } + if (!callback) { + callback = options; + } + + var diskPath = 'projects/' + + this.compute.projectId + + '/zones/' + + disk.zone + + '/disks/' + + disk.name; + + var body = {}; + + body.source = diskPath; + + if (util.is(options.index, 'number')) { + body.index = options.index; + } + if (options.readOnly) { + body.mode = 'READ_ONLY'; + } + if (options.deviceName) { + body.deviceName = options.deviceName; + } + if (options.boot) { + body.boot = options.boot; + } + if (options.autoDelete) { + body.autoDelete = options.autoDelete; + } + + this.makeReq_('POST', '/attachDisk', null, body, callback); +}; + +/** + * Detach a disk from the instance. + * + * @param {string} deviceName - The name of the device to detach. + * @param {function} callback - The callback function. + * + * @example + * instance.detachDisk( + * disk, + * { + * readOnly : true + * } + * function(err, apiResponse) {}); + */ +Instance.prototype.detachDisk = function(deviceName, callback) { + if (!util.is(deviceName, 'string')) { + throw new Error('A device name must be provided.'); + } + + var query = {}; + query.deviceName = deviceName; + + this.makeReq_('POST', '/detachDisk', query, null, callback); +}; + +/** + * Returns the serial port output for the instance. + * + * @param {number=} port - The port from which the output is retrieved. + * A number in the interval [1,4] (default is 1). + * @param {function} callback - The callback function. + * + * @example + * instance.getSerialPortOutput( + * 4, + * function(err, output) {}); + */ +Instance.prototype.getSerialPortOutput = function(port, callback) { + if (!callback) { + callback = port; + port = 1; + } + if (!util.is(port, 'number') || port < 1 || port > 4) { + throw new Error('Port must be a number between 1 and 4 (inclusive).'); + } + + var query = {}; + query.port = port; + + this.makeReq_( + 'GET', + '/serialPort', + query, + null, function(err, resp) { + if (err) { + callback(err); + return; + } + callback(null, resp.content); + }.bind(this)); +}; + +/** + * Reset the instance. + * + * @param {function} callback - The callback function. + * + * @example + * instance.reset(function(err, apiResponse) {}); + */ +Instance.prototype.reset = function(callback) { + callback = callback || util.noop; + this.makeReq_('POST', '/reset', null, null, callback); +}; + +/** + * Start the instance. + * + * @param {function} callback - The callback function. + * + * @example + * instance.start(function(err, apiResponse) {}); + */ +Instance.prototype.start = function(callback) { + callback = callback || util.noop; + this.makeReq_('POST', '/start', null, null, callback); +}; + +/** + * Stop the instance. + * + * @param {function} callback - The callback function. + * + * @example + * instance.stop(function(err, apiResponse) {}); + */ +Instance.prototype.stop = function(callback) { + callback = callback || util.noop; + this.makeReq_('POST', '/stop', null, null, callback); +}; + +/** + * Get instance's tags and tags fingerprint. + * + * @param {function} callback - The callback function. + * + * @example + * instance.getTags(function(err, tags, fingerprint) {}); + */ +Instance.prototype.getTags = function(callback) { + if (!callback) { + throw new Error('A callback must be provided to get tags'); + } + this.makeReq_('GET', '', null, null, function(err, resp) { + if (err) { + callback(err); + return; + } + this.metadata = resp; + callback(null, resp.tags.items, resp.tags.fingerprint); + }.bind(this)); +}; + +/** + * Set instance's tags. + * + * @param {{string[]} tags - The new tags for the instance. + * @param {string} fingerprint - The current tags fingerprint. Up-to-date + * fingerprint must be provided to set tags. + * @param {function} callback - The callback function. + * + * @example + * instance.getTags(function(err, tags, fingerprint) { + * if (err) throw err; + * tags.push('new-tag'); + * instance.setTags(tags, fingerprint, function(err, apiResponse) {}); + * }); + */ +Instance.prototype.setTags = function(tags, fingerprint, callback) { + callback = callback || util.noop; + if (!util.is(tags, 'array')) { + throw new Error('You must provide an array of tags.'); + } + if (!util.is(fingerprint, 'string')) { + throw new Error('You must provide a fingerprint.'); + } + for(var i = 0; i < tags.length; i++) { + if (!/^(?:[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?)$/.test(tags[i])) { + throw new Error('Tags must match [a-z]([-a-z0-9]{0,61}[a-z0-9])?'); + } + } + + var body = {}; + body.items = tags; + body.fingerprint = fingerprint; + + this.makeReq_('POST', '/setTags', null, body, callback); +}; + +/** + * Make a new request object from the provided arguments and wrap the callback + * to intercept non-successful responses. + * + * @private + * + * @param {string} method - Action. + * @param {string} path - Request path. + * @param {*} query - Request query object. + * @param {*} body - Request body contents. + * @param {function} callback - The callback function. + */ +Instance.prototype.makeReq_ = function(method, path, query, body, callback) { + var reqOpts = { + method: method, + qs: query, + uri: COMPUTE_BASE_URL + this.compute.projectId + + '/zones/' + this.zone + + '/instances/' + this.name + path + }; + + if (body) { + reqOpts.json = body; + } + + this.compute.makeAuthorizedRequest_(reqOpts, callback); +}; + +module.exports = Instance; diff --git a/lib/index.js b/lib/index.js index dedd6f70f80..95fcab10c2e 100644 --- a/lib/index.js +++ b/lib/index.js @@ -56,6 +56,12 @@ var Search = require('./search'); */ var Storage = require('./storage'); +/** + * @type {module:storage} + * @private + */ +var Compute = require('./compute'); + /** * @type {module:common/util} * @private @@ -143,6 +149,10 @@ function gcloud(config) { storage: function(options) { options = options || {}; return new Storage(util.extendGlobalConfig(config, options)); + }, + compute: function(options) { + options = options || {}; + return new Compute(util.extendGlobalConfig(config, options)); } }; } @@ -259,4 +269,6 @@ gcloud.search = function(config) { */ gcloud.storage = Storage; +gcloud.compute = Compute; + module.exports = gcloud; From f4b3f520a63853f41a233ccd7dad2bb8178041d6 Mon Sep 17 00:00:00 2001 From: Marco Ziccardi Date: Thu, 16 Jul 2015 15:37:32 +0200 Subject: [PATCH 2/9] Add zone and region entities to provide a more hierarchical structure --- lib/compute/disk.js | 29 +- lib/compute/index.js | 472 +++++++-------------------------- lib/compute/instance.js | 27 +- lib/compute/region.js | 86 ++++++ lib/compute/zone.js | 570 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 785 insertions(+), 399 deletions(-) create mode 100644 lib/compute/region.js create mode 100644 lib/compute/zone.js diff --git a/lib/compute/disk.js b/lib/compute/disk.js index c5916735c72..a4ec1f118d9 100644 --- a/lib/compute/disk.js +++ b/lib/compute/disk.js @@ -36,31 +36,31 @@ var COMPUTE_BASE_URL = 'https://www.googleapis.com/compute/v1/projects/'; * Create a Disk object to interact with a Google Compute Engine disk. * * @constructor - * @alias module:compute/disk + * @alias module:zone/disk * * @throws {Error} if a disk name or a zone are not provided. * - * @param {module:compute} compute - The Google Compute Engine instance this + * @param {module:zone} zone - The Google Compute Engine zone this * disk belongs to. - * @param {string} name - Name of the disk. - * @param {string} zone - Zone to which the disk belongs. * @param {number=} sizeGb - Size of the disk in GB. * @param {object=} metadata - Disk metadata. * * @example - * var gcloud = require('gcloud'); - * - * var compute = gcloud.compute({ - * projectId: 'grape-spaceship-123' + * var gcloud = require('gcloud')({ + * keyFilename: '/path/to/keyfile.json' * }); * - * var disk = compute.disk('disk-1', 'europe-west1-b'); + * var compute = gcloud.compute(); + * + * var myZone = compute.zone('zone-name'); + * + * var disk = myZone.disk('disk1'); */ -function Disk(compute, name, zone, sizeGb, metadata) { +function Disk(zone, name, sizeGb, metadata) { this.name = name; this.zone = zone; - if (!this.name) { + if (!util.is(this.name, 'string')) { throw new Error('A name is needed to use Google Cloud Compute Disk.'); } if (!this.zone) { @@ -68,7 +68,6 @@ function Disk(compute, name, zone, sizeGb, metadata) { } this.sizeGb = sizeGb; - this.compute = compute; this.metadata = metadata; } @@ -132,8 +131,8 @@ Disk.prototype.makeReq_ = function(method, path, query, body, callback) { var reqOpts = { method: method, qs: query, - uri: COMPUTE_BASE_URL + this.compute.projectId + - '/zones/' + this.zone + + uri: COMPUTE_BASE_URL + this.zone.compute.projectId + + '/zones/' + this.zone.name + '/disks/' + this.name + path }; @@ -141,7 +140,7 @@ Disk.prototype.makeReq_ = function(method, path, query, body, callback) { reqOpts.json = body; } - this.compute.makeAuthorizedRequest_(reqOpts, callback); + this.zone.compute.makeAuthorizedRequest_(reqOpts, callback); }; module.exports = Disk; \ No newline at end of file diff --git a/lib/compute/index.js b/lib/compute/index.js index 27db32fb5be..397fb6a1ef1 100644 --- a/lib/compute/index.js +++ b/lib/compute/index.js @@ -20,6 +20,18 @@ 'use strict'; +/** + * @type {module:compute/region} + * @private + */ +var Region = require('./region.js'); + +/** + * @type {module:compute/zone} + * @private + */ +var Zone = require('./zone.js'); + /** * @type {module:compute/instance} * @private @@ -100,11 +112,10 @@ function Compute(options) { } /** - * Get a reference to a Google Compute Engine instance. + * Get a reference to a Google Compute Engine region. * - * @param {string} name - Name of the existing instance. - * @param {string} zone - Zone of the existing instance. - * @return {module:compute/instance} + * @param {string} name - Name of the region. + * @return {module:compute/region} * * @example * var gcloud = require('gcloud')({ @@ -113,18 +124,17 @@ function Compute(options) { * * var compute = gcloud.compute(); * - * var instance = compute.instance('instance1', 'europe-west1-b'); + * var myRegion = compute.region('region-name'); */ -Compute.prototype.instance = function(name, zone) { - return new Instance(this, name, zone); +Compute.prototype.region = function(name) { + return new Region(this, name); }; /** - * Get a reference to a Google Compute Engine disk. + * Get a reference to a Google Compute Engine zone. * - * @param {string} name - Name of the existing disk. - * @param {string} zone - Zone of the existing disk. - * @return {module:compute/disk} + * @param {string} name - Name of the zone. + * @return {module:compute/zone} * * @example * var gcloud = require('gcloud')({ @@ -133,10 +143,10 @@ Compute.prototype.instance = function(name, zone) { * * var compute = gcloud.compute(); * - * var disk = compute.disk('disk1', 'europe-west1-b'); + * var myZone = compute.zone('zone-name'); */ -Compute.prototype.disk = function(name, zone) { - return new Disk(this, name, zone); +Compute.prototype.zone = function(name) { + return new Zone(this, name); }; /** @@ -165,245 +175,6 @@ Compute.prototype.firewall = function(name, networkName) { return new Firewall(this, name); }; -/** - * Create an instance. - * - * @throws {Error} if an instance name or options are not provided. - * - * @param {string} name - Name of the instance. - * @param {object} options - Options for instance creation. - * @param {string} options.zone - Zone where the instance is created. - * @param {string} machineType - Type of the instance. - * @param {object[]} disks - Disks to attach to the instance. - * @param {boolean} disks[].createNew - True if the disk has to be created. - * Default value is false. - * @param {string} disks[].source - Source for the disk to be created, either an - * image or a snapshot. - * @param {string=} disks[].diskType - Type of the disk. - * @param {number=} disks[].sizeGb - Size of the disk in GB. - * @param {boolean=} disks[].boot - True of the disk is a boot disk. - * @param {string=} disks[].mode - Attach mode, either `READ_ONLY` or - * `READ_WRITE`. Default value is `READ_WRITE`. - * @param {number=} disks[].index - Zero based index assigned from the instance - * to the disk. - * @param {string=} disks[].deviceName - Device name assigned from the instance - * to the disk. - * @param {boolean=} disks[].autoDelete - If true the disk is deleted when the - * instance is deleted. - * @param {object[]} networkInterfaces - Network interfaces for the instance. - * @param {string} networkInterfaces[].network - Full/partial URL of a network - * interface for this instance. - * @param {function} callback - The callback function. - * - * @example - * var callback = function(err, instance, apiResponse) { - * // `instance` is an Instance object. - * }; - * - * var compute = gcloud.compute({ - * projectId: 'grape-spaceship-123' - * }); - * - * compute.createInstance( - * 'instance-1', - * { - * machineType: 'zones/europe-west1-b/machineTypes/n1-standard-1', - * zone: 'europe-west1-b', - * disks: [{ - * createNew: true, - * diskName: 'test-create-disk', - * source: - * 'projects/debian-cloud/global/images/debian-7-wheezy-v20150325', - * diskSizeGb: 10, - * boot: true, - * autoDelete: true - * }], - * networkInterfaces: [{ - * network: 'global/networks/default' - * }] - * }, callback); - */ -Compute.prototype.createInstance = function(name, options, callback) { - if (!name) { - throw new Error('A name is required to create an instance.'); - } else if (!/^(?:[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?)$/.test(name)) { - throw new Error('Name must match [a-z]([-a-z0-9]{0,61}[a-z0-9])?'); - } - if (!options) { - throw new Error('Options are required to create an instance.'); - } - if (!options.zone) { - throw new Error('A zone is required to create an instance.'); - } - if (!options.machineType) { - throw new Error('A machine type is required to create an instance.'); - } - if (!callback) { - throw new Error('A callback must be defined.'); - } - - var query = {}; - - var body = { - name: name, - machineType: options.machineType, - disks: [], - networkInterfaces: [] - }; - - if (!util.is(options.disks, 'array')) { - options.disks = [options.disks]; - } - - options.disks.forEach(function(disk) { - var requestDisk = {}; - if (util.is(disk.source, 'string')) { - if (disk.createNew) { - requestDisk.initializeParams = { - sourceImage: disk.source - }; - } else { - requestDisk.source = disk.source; - } - } else { - throw new Error('A disk source must be provided.'); - } - if (util.is(disk.diskSizeGb, 'long')) { - requestDisk.initializeParams.diskSizeGb = disk.diskSizeGb; - } - if (util.is(disk.diskType, 'string')) { - requestDisk.initializeParams.diskType = disk.diskType; - } - if (util.is(disk.diskName, 'string')) { - requestDisk.initializeParams.diskName = disk.diskName; - } - if (util.is(disk.type, 'string')) { - requestDisk.type = disk.type; - } - if (util.is(disk.mode, 'string')) { - requestDisk.mode = disk.mode; - } - if (util.is(disk.index, 'integer')) { - requestDisk.index = disk.index; - } - if (util.is(disk.deviceName, 'string')) { - requestDisk.deviceName = disk.deviceName; - } - if (util.is(disk.boot, 'boolean')) { - requestDisk.boot = disk.boot; - } - if (util.is(disk.autoDelete, 'boolean')) { - requestDisk.autoDelete = disk.autoDelete; - } - body.disks.push(requestDisk); - }); - if (body.disks.lenght === 0) { - throw new Error('A disk must be provided to create an instance.'); - } - if (!util.is(options.networkIntefaces, 'array')) { - options.networkIntefaces = [options.networks]; - } - options.networkInterfaces.forEach(function(networkInterface) { - body.networkInterfaces.push({ - network: networkInterface.network - }); - }); - - this.makeReq_( - 'POST', - '/zones/'+options.zone+'/instances', - query, - body, function(err, resp) { - if (err) { - callback(err); - return; - } - var instance = new Instance(this, name, options.zone); - callback(null, instance, resp); - }.bind(this)); -}; - -/** - * Create a disk. - * - * @throws {Error} if a disk name or options are not provided or if both a - * source image and a source snapshot are provided. - * - * @param {string} name - Name of the disk. - * @param {object} options - Options for disk creation. - * @param {string} options.zone - Zone where the disk is created. - * @param {string=} sourceImage - Source image for the disk. - * @param {string=} sourceSnapshot - Source snapshot for the disk. - * @param {number=} sizeGb - Size of the disk in GB. - * @param {function} callback - The callback function. - * - * @example - * var callback = function(err, disk, apiResponse) { - * // `disk` is a Disk object. - * }; - * - * var compute = gcloud.compute({ - * projectId: 'grape-spaceship-123' - * }); - * - * compute.createDisk( - * 'new-disk', - * { - * zone: 'europe-west1-b', - * sizeGb: 10 - * }, callback); - */ -Compute.prototype.createDisk = function(name, options, callback) { - if (!name) { - throw new Error('A name is required to create a disk.'); - } else if (!/^(?:[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?)$/.test(name)) { - throw new Error('Name must match [a-z]([-a-z0-9]{0,61}[a-z0-9])?'); - } - if (!options) { - throw new Error('Options are required to create a disk.'); - } - if (!options.zone) { - throw new Error('A zone is required to create a disk.'); - } - if (!options.sourceImage && !options.sizeGb && !options.sourceSnapshot) { - throw new Error('An image, a snapshot or the size is required.'); - } - if (options.sourceImage && options.sourceSnapshot) { - throw new Error('An image and a snapshot can not be both provided.'); - } - if (!callback) { - throw new Error('A callback must be defined.'); - } - - var query = {}; - var body = {}; - - if (options.sourceImage) { - query.sourceImage = options.sourceImage; - } else if (options.sourceSnapshot) { - body.sourceSnapshot = options.sourceSnapshot; - } - - if (util.is(options.sizeGb, 'number')) { - body.sizeGb = options.sizeGb.toString(); - } - - body.name = name; - - this.makeReq_( - 'POST', - '/zones/'+options.zone+'/disks', - query, - body, function(err, resp) { - if (err) { - callback(err); - return; - } - var disk = new Disk(this, name, options.zone, options.sizeGb); - callback(null, disk, resp); - }.bind(this)); -}; - /** * Create a firewall rule. * @@ -544,69 +315,34 @@ Compute.prototype.listInstances = function(options, callback) { var query = {}; - if (util.is(options.maxResults, 'number')) { - query.maxResults = options.maxResults; - } - if (options.filter) { - if (!util.is(options.filter.fieldName, 'string') || - !util.is(options.filter.operatorString, 'string') || - !util.is(options.filter.literalString, 'string')) { - throw new Error( - 'A filter must have name, comparisonString and literal fields.'); - } - if (options.filter.operatorString !== 'eq' && - options.filter.operatorString !== 'ne') { - throw new Error('Filter operator must be either eq or ne'); + if (options) { + if (util.is(options.maxResults, 'number')) { + query.maxResults = options.maxResults; } - query.filter = - options.filter.fieldName + ' ' + - options.filter.operatorString + ' \'' + - options.filter.literalString + '\''; - } - if (util.is(options.pageToken, 'string')) { - query.pageToken = options.pageToken; - } - - var path = ''; - var processResponse; - if (options.zone) { - path = '/zones/'+options.zone; - processResponse = function(resp) { - var instances = []; - for (var i = 0; i < resp.items.length; i++) { - var instance = new Instance( - this, - resp.items[i].name, - resp.items[i].zone, - resp.items[i]); - instances.push(instance); + if (options.filter) { + if (!util.is(options.filter.fieldName, 'string') || + !util.is(options.filter.operatorString, 'string') || + !util.is(options.filter.literalString, 'string')) { + throw new Error( + 'A filter must have name, comparisonString and literal fields.'); } - callback(null, instances); - }.bind(this); - } else { - path = '/aggregated'; - processResponse = function(resp) { - var instances = []; - for (var zone in resp.items) { - var zoneInstances = resp.items[zone].instances; - if (zoneInstances) { - for (var i = 0; i < zoneInstances.length; i++) { - var instance = new Instance( - this, - zoneInstances[i].name, - zoneInstances[i].zone, - zoneInstances[i]); - instances.push(instance); - } - } + if (options.filter.operatorString !== 'eq' && + options.filter.operatorString !== 'ne') { + throw new Error('Filter operator must be either eq or ne'); } - callback(null, instances); - }.bind(this); + query.filter = + options.filter.fieldName + ' ' + + options.filter.operatorString + ' \'' + + options.filter.literalString + '\''; + } + if (util.is(options.pageToken, 'string')) { + query.pageToken = options.pageToken; + } } this.makeReq_( 'GET', - path+'/instances', + '/aggregated/instances', query, null, function(err, resp) { if (err) { @@ -614,11 +350,25 @@ Compute.prototype.listInstances = function(options, callback) { return; } if (resp.items) { - processResponse(resp); + var instances = []; + for (var zoneName in resp.items) { + var zone = this.zone(zoneName); + var zoneInstances = resp.items[zoneName].instances; + if (zoneInstances) { + for (var i = 0; i < zoneInstances.length; i++) { + var instance = new Instance( + zone, + zoneInstances[i].name, + zoneInstances[i]); + instances.push(instance); + } + } + } + callback(null, instances); } else { callback(null, []); } - }); + }.bind(this)); }; /** @@ -665,71 +415,34 @@ Compute.prototype.listDisks = function(options, callback) { var query = {}; - if (util.is(options.maxResults, 'number')) { - query.maxResults = options.maxResults; - } - if (options.filter) { - if (!util.is(options.filter.fieldName, 'string') || - !util.is(options.filter.operatorString, 'string') || - !util.is(options.filter.literalString, 'string')) { - throw new Error( - 'A filter must have name, comparisonString and literal fields.'); - } - if (options.filter.operatorString !== 'eq' && - options.filter.operatorString !== 'ne') { - throw new Error('Filter operator must be either eq or ne'); + if (options) { + if (util.is(options.maxResults, 'number')) { + query.maxResults = options.maxResults; } - query.filter = - options.filter.fieldName + ' ' + - options.filter.operatorString + ' \'' + - options.filter.literalString + '\''; - } - if (util.is(options.pageToken, 'string')) { - query.pageToken = options.pageToken; - } - - var path = ''; - var processResponse; - if (options.zone) { - path = '/zones/'+options.zone; - processResponse = function(resp) { - var disks = []; - for (var i = 0; i < resp.items.length; i++) { - var disk = new Disk( - this, - resp.items[i].name, - resp.items[i].zone, - resp.items[i].sizeGb, - resp.items[i]); - disks.push(disk); + if (options.filter) { + if (!util.is(options.filter.fieldName, 'string') || + !util.is(options.filter.operatorString, 'string') || + !util.is(options.filter.literalString, 'string')) { + throw new Error( + 'A filter must have name, comparisonString and literal fields.'); } - callback(null, disks); - }.bind(this); - } else { - path = '/aggregated'; - processResponse = function(resp) { - var disks = []; - for (var zone in resp.items) { - var zoneDisks = resp.items[zone].disks; - if (zoneDisks) { - for (var i = 0; i < zoneDisks.length; i++) { - var disk = new Disk( - this, - zoneDisks[i].name, - zoneDisks[i].zone, - zoneDisks[i].sizeGb, - zoneDisks[i]); - disks.push(disk); - } - } + if (options.filter.operatorString !== 'eq' && + options.filter.operatorString !== 'ne') { + throw new Error('Filter operator must be either eq or ne'); } - callback(null, disks); - }.bind(this); + query.filter = + options.filter.fieldName + ' ' + + options.filter.operatorString + ' \'' + + options.filter.literalString + '\''; + } + if (util.is(options.pageToken, 'string')) { + query.pageToken = options.pageToken; + } } - + this.makeReq_( 'GET', - path+'/disks', + '/aggregated/disks', query, null, function(err, resp) { if (err) { @@ -737,11 +450,26 @@ Compute.prototype.listDisks = function(options, callback) { return; } if (resp.items) { - processResponse(resp); + var disks = []; + for (var zoneName in resp.items) { + var zone = this.zone(zoneName); + var zoneDisks = resp.items[zoneName].disks; + if (zoneDisks) { + for (var i = 0; i < zoneDisks.length; i++) { + var disk = new Disk( + zone, + zoneDisks[i].name, + zoneDisks[i].sizeGb, + zoneDisks[i]); + disks.push(disk); + } + } + } + callback(null, disks); } else { callback(null, []); } - }); + }.bind(this)); }; /** diff --git a/lib/compute/instance.js b/lib/compute/instance.js index d55390eea04..d55ad6d4ea7 100644 --- a/lib/compute/instance.js +++ b/lib/compute/instance.js @@ -33,17 +33,16 @@ var util = require('../common/util.js'); var COMPUTE_BASE_URL = 'https://www.googleapis.com/compute/v1/projects/'; /** - * Create a Instance object to interact with a Google Compute Engine disk. + * Create a Instance object to interact with a Google Compute Engine instance. * * @constructor * @alias module:compute/istance * - * @throws {Error} if a disk name or a zone are not provided. + * @throws {Error} if an instance name or a zone are not provided. * - * @param {module:compute} compute - The Google Compute Engine instance this + * @param {module:zone} zone - The Google Compute Engine Zone this * instance belongs to. * @param {string} name - Name of the instance. - * @param {string} zone - Zone to which the instance belongs. * @param {object=} metadata - Instance metadata. * * @example @@ -53,17 +52,21 @@ var COMPUTE_BASE_URL = 'https://www.googleapis.com/compute/v1/projects/'; * projectId: 'grape-spaceship-123' * }); * - * var instance = compute.instance('instance-1', 'europe-west1-b'); + * var myZone = compute.zone('zone-name'); + * + * var instance = myZone.instance('instance1'); */ -function Instance(compute, name, zone, metadata) { +function Instance(zone, name, metadata) { this.name = name; this.zone = zone; - this.compute = compute; this.metadata = metadata; - if (!this.name) { + if (!util.is(this.name, 'string')) { throw Error('An instance name is needed to create a Compute Instance.'); } + if (!this.zone) { + throw Error('A zone is needed to create a Compute Instance.'); + } } /** @@ -116,7 +119,7 @@ Instance.prototype.attachDisk = function(disk, options, callback) { var diskPath = 'projects/' + this.compute.projectId + '/zones/' + - disk.zone + + disk.zone.name + '/disks/' + disk.name; @@ -318,8 +321,8 @@ Instance.prototype.makeReq_ = function(method, path, query, body, callback) { var reqOpts = { method: method, qs: query, - uri: COMPUTE_BASE_URL + this.compute.projectId + - '/zones/' + this.zone + + uri: COMPUTE_BASE_URL + this.zone.compute.projectId + + '/zones/' + this.zone.name + '/instances/' + this.name + path }; @@ -327,7 +330,7 @@ Instance.prototype.makeReq_ = function(method, path, query, body, callback) { reqOpts.json = body; } - this.compute.makeAuthorizedRequest_(reqOpts, callback); + this.zone.compute.makeAuthorizedRequest_(reqOpts, callback); }; module.exports = Instance; diff --git a/lib/compute/region.js b/lib/compute/region.js new file mode 100644 index 00000000000..1beeec847c3 --- /dev/null +++ b/lib/compute/region.js @@ -0,0 +1,86 @@ +/*! + * Copyright 2014 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*! + * @module compute/region + */ + +'use strict'; + +/** + * @const {string} + * @private + */ +var COMPUTE_BASE_URL = 'https://www.googleapis.com/compute/v1/projects/'; + +/** + * Create a Region object to interact with a Google Compute Engine region. + * + * @constructor + * @alias module:compute/region + * + * @throws {Error} if a region name is not provided. + * + * @param {module:compute} compute - The Google Compute Engine instance this + * disk belongs to. + * @param {string} name - Name of the region. + * + * @example + * var gcloud = require('gcloud'); + * + * var compute = gcloud.compute({ + * projectId: 'grape-spaceship-123' + * }); + * + * var myRegion = compute.region('region-name'); + */ +function Region(compute, name) { + this.name = name; + this.compute = compute; + + if (!this.name) { + throw new Error('A name is needed to use Google Cloud Compute Region.'); + } +} + +/** + * Make a new request object from the provided arguments and wrap the callback + * to intercept non-successful responses. + * + * @private + * + * @param {string} method - Action. + * @param {string} path - Request path. + * @param {*} query - Request query object. + * @param {*} body - Request body contents. + * @param {function} callback - The callback function. + */ +Region.prototype.makeReq_ = function(method, path, query, body, callback) { + var reqOpts = { + method: method, + qs: query, + uri: COMPUTE_BASE_URL + this.compute.projectId + + '/regions/' + this.name + path + }; + + if (body) { + reqOpts.json = body; + } + + this.compute.makeAuthorizedRequest_(reqOpts, callback); +}; + +module.exports = Region; \ No newline at end of file diff --git a/lib/compute/zone.js b/lib/compute/zone.js new file mode 100644 index 00000000000..49d12ea7795 --- /dev/null +++ b/lib/compute/zone.js @@ -0,0 +1,570 @@ +/*! + * Copyright 2014 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*! + * @module compute/zone + */ + +'use strict'; + +/** + * @type {module:compute/instance} + * @private + */ +var Instance = require('./instance.js'); + +/** + * @type {module:compute/disk} + * @private + */ +var Disk = require('./disk.js'); + +/** + * @type {module:common/util} + * @private + */ +var util = require('../common/util.js'); + +/** + * @const {string} + * @private + */ +var COMPUTE_BASE_URL = 'https://www.googleapis.com/compute/v1/projects/'; + +/** + * Create a Zone object to interact with a Google Compute Engine zone. + * + * @constructor + * @alias module:compute/zone + * + * @throws {Error} if a zone name is not provided. + * + * @param {module:compute} compute - The Google Compute Engine instance this + * disk belongs to. + * @param {string} name - Name of the zone. + * + * @example + * var gcloud = require('gcloud'); + * + * var compute = gcloud.compute({ + * projectId: 'grape-spaceship-123' + * }); + * + * var myZone = compute.zone('zone-name'); + */ +function Zone(compute, name) { + this.name = name; + this.compute = compute; + + if (!this.name) { + throw new Error('A name is needed to use Google Cloud Compute Zone.'); + } +} + +/** + * Get a reference to a Google Compute Engine instance in this zone. + * + * @param {string} name - Name of the existing instance. + * @return {module:compute/instance} + * + * @example + * var gcloud = require('gcloud')({ + * keyFilename: '/path/to/keyfile.json' + * }); + * + * var compute = gcloud.compute(); + * + * var myZone = compute.zone('zone-name'); + * + * var instance = myZone.instance('instance1'); + */ +Zone.prototype.instance = function(name) { + return new Instance(this, name); +}; + +/** + * Get a reference to a Google Compute Engine disk in this zone. + * + * @param {string} name - Name of the existing disk. + * @return {module:compute/disk} + * + * @example + * var gcloud = require('gcloud')({ + * keyFilename: '/path/to/keyfile.json' + * }); + * + * var compute = gcloud.compute(); + * + * var myZone = compute.zone('zone-name'); + * + * var disk = myZone.disk('disk1'); + */ +Zone.prototype.disk = function(name) { + return new Disk(this, name); +}; + +/** + * Create an instance in the zone. + * + * @throws {Error} if an instance name or options are not provided. + * + * @param {string} name - Name of the instance. + * @param {object} options - Options for instance creation. + * @param {string} machineType - Type of the instance. + * @param {object[]} disks - Disks to attach to the instance. + * @param {boolean} disks[].createNew - True if the disk has to be created. + * Default value is false. + * @param {string} disks[].source - Source for the disk to be created, either an + * image or a snapshot. + * @param {string=} disks[].diskType - Type of the disk. + * @param {number=} disks[].sizeGb - Size of the disk in GB. + * @param {boolean=} disks[].boot - True of the disk is a boot disk. + * @param {string=} disks[].mode - Attach mode, either `READ_ONLY` or + * `READ_WRITE`. Default value is `READ_WRITE`. + * @param {number=} disks[].index - Zero based index assigned from the instance + * to the disk. + * @param {string=} disks[].deviceName - Device name assigned from the instance + * to the disk. + * @param {boolean=} disks[].autoDelete - If true the disk is deleted when the + * instance is deleted. + * @param {object[]} networkInterfaces - Network interfaces for the instance. + * @param {string} networkInterfaces[].network - Full/partial URL of a network + * interface for this instance. + * @param {function} callback - The callback function. + * + * @example + * var callback = function(err, instance, apiResponse) { + * // `instance` is an Instance object. + * }; + * + * var compute = gcloud.compute({ + * projectId: 'grape-spaceship-123' + * }); + * + * var myZone = compute.zone('zone-name'); + * + * myZone.createInstance( + * 'instance-1', + * { + * machineType: 'zones/europe-west1-b/machineTypes/n1-standard-1', + * disks: [{ + * createNew: true, + * diskName: 'test-create-disk', + * source: + * 'projects/debian-cloud/global/images/debian-7-wheezy-v20150325', + * diskSizeGb: 10, + * boot: true, + * autoDelete: true + * }], + * networkInterfaces: [{ + * network: 'global/networks/default' + * }] + * }, callback); + */ +Zone.prototype.createInstance = function(name, options, callback) { + if (!name) { + throw new Error('A name is required to create an instance.'); + } else if (!/^(?:[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?)$/.test(name)) { + throw new Error('Name must match [a-z]([-a-z0-9]{0,61}[a-z0-9])?'); + } + if (!options) { + throw new Error('Options are required to create an instance.'); + } + if (!options.machineType) { + throw new Error('A machine type is required to create an instance.'); + } + if (!callback) { + throw new Error('A callback must be defined.'); + } + + var query = {}; + + var body = { + name: name, + machineType: options.machineType, + disks: [], + networkInterfaces: [] + }; + + if (!util.is(options.disks, 'array')) { + options.disks = [options.disks]; + } + + options.disks.forEach(function(disk) { + var requestDisk = {}; + if (util.is(disk.source, 'string')) { + if (disk.createNew) { + requestDisk.initializeParams = { + sourceImage: disk.source + }; + } else { + requestDisk.source = disk.source; + } + } else { + throw new Error('A disk source must be provided.'); + } + if (util.is(disk.diskSizeGb, 'long')) { + requestDisk.initializeParams.diskSizeGb = disk.diskSizeGb; + } + if (util.is(disk.diskType, 'string')) { + requestDisk.initializeParams.diskType = disk.diskType; + } + if (util.is(disk.diskName, 'string')) { + requestDisk.initializeParams.diskName = disk.diskName; + } + if (util.is(disk.type, 'string')) { + requestDisk.type = disk.type; + } + if (util.is(disk.mode, 'string')) { + requestDisk.mode = disk.mode; + } + if (util.is(disk.index, 'integer')) { + requestDisk.index = disk.index; + } + if (util.is(disk.deviceName, 'string')) { + requestDisk.deviceName = disk.deviceName; + } + if (util.is(disk.boot, 'boolean')) { + requestDisk.boot = disk.boot; + } + if (util.is(disk.autoDelete, 'boolean')) { + requestDisk.autoDelete = disk.autoDelete; + } + body.disks.push(requestDisk); + }); + if (body.disks.lenght === 0) { + throw new Error('A disk must be provided to create an instance.'); + } + if (!util.is(options.networkIntefaces, 'array')) { + options.networkIntefaces = [options.networks]; + } + options.networkInterfaces.forEach(function(networkInterface) { + body.networkInterfaces.push({ + network: networkInterface.network + }); + }); + + this.makeReq_( + 'POST', + '/instances', + query, + body, function(err, resp) { + if (err) { + callback(err); + return; + } + var instance = new Instance(this, name); + callback(null, instance, resp); + }.bind(this)); +}; + +/** + * Get a list of instances in this zone. + * + * @throws {Error} if a malformed filter is provided. + * + * @param {object} options - Instance search options. + * @param {number=} options.maxResults - Maximum number of instances to + * return. + * @param {object=} options.filter - Search filter. + * @param {string} options.filter.fieldName - Instance field to consider in this + * filter. + * @param {string} options.filter.operatorString - Filter operator, can be + * either 'eq' or 'ne'. + * @param {string} options.filter.literalString - String to compare the instance + * field to. Can be a regular expression. + * @param {string=} pageToken - Page identifier used in paginated search. + * @param {function} callback - The callback function. + * + * @example + * var callback = function(err, instances) { + * // `instances` is an array of `Instance` objects. + * }; + * + * var compute = gcloud.compute({ + * projectId: 'grape-spaceship-123' + * }); + * + * var myZone = compute.zone('zone-name'); + * + * myZone.listInstances( + * { + * filter: { + * fieldName : 'name', + * operatorString : 'eq', + * literalString : 'instance-[0-9]' + * }], + * }, callback); + */ +Zone.prototype.listInstances = function(options, callback) { + if (!callback) { + callback = options; + } + + var query = {}; + + if (options) { + if (util.is(options.maxResults, 'number')) { + query.maxResults = options.maxResults; + } + if (options.filter) { + if (!util.is(options.filter.fieldName, 'string') || + !util.is(options.filter.operatorString, 'string') || + !util.is(options.filter.literalString, 'string')) { + throw new Error( + 'A filter must have name, comparisonString and literal fields.'); + } + if (options.filter.operatorString !== 'eq' && + options.filter.operatorString !== 'ne') { + throw new Error('Filter operator must be either eq or ne'); + } + query.filter = + options.filter.fieldName + ' ' + + options.filter.operatorString + ' \'' + + options.filter.literalString + '\''; + } + if (util.is(options.pageToken, 'string')) { + query.pageToken = options.pageToken; + } + } + + this.makeReq_( + 'GET', + '/instances', + query, + null, function(err, resp) { + if (err) { + callback(err); + return; + } + console.log(resp); + if (resp.items) { + var instances = []; + for (var i = 0; i < resp.items.length; i++) { + var instance = new Instance( + this, + resp.items[i].name, + resp.items[i]); + instances.push(instance); + } + callback(null, instances); + } else { + callback(null, []); + } + }.bind(this)); +}; + +/** + * Create a disk in this zone. + * + * @throws {Error} if a disk name or options are not provided or if both a + * source image and a source snapshot are provided. + * + * @param {string} name - Name of the disk. + * @param {object} options - Options for disk creation. + * @param {string=} sourceImage - Source image for the disk. + * @param {string=} sourceSnapshot - Source snapshot for the disk. + * @param {number=} sizeGb - Size of the disk in GB. + * @param {function} callback - The callback function. + * + * @example + * var callback = function(err, disk, apiResponse) { + * // `disk` is a Disk object. + * }; + * + * var compute = gcloud.compute({ + * projectId: 'grape-spaceship-123' + * }); + * + * var myZone = compute.zone('zone-name'); + * + * myZone.createDisk( + * 'new-disk', + * { + * sizeGb: 10 + * }, callback); + */ +Zone.prototype.createDisk = function(name, options, callback) { + if (!name) { + throw new Error('A name is required to create a disk.'); + } else if (!/^(?:[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?)$/.test(name)) { + throw new Error('Name must match [a-z]([-a-z0-9]{0,61}[a-z0-9])?'); + } + if (!options) { + throw new Error('Options are required to create a disk.'); + } + if (!options.sourceImage && !options.sizeGb && !options.sourceSnapshot) { + throw new Error('An image, a snapshot or the size is required.'); + } + if (options.sourceImage && options.sourceSnapshot) { + throw new Error('An image and a snapshot can not be both provided.'); + } + if (!callback) { + throw new Error('A callback must be defined.'); + } + + var query = {}; + var body = {}; + + if (options.sourceImage) { + query.sourceImage = options.sourceImage; + } else if (options.sourceSnapshot) { + body.sourceSnapshot = options.sourceSnapshot; + } + + if (util.is(options.sizeGb, 'number')) { + body.sizeGb = options.sizeGb.toString(); + } + + body.name = name; + + this.makeReq_( + 'POST', + '/disks', + query, + body, function(err, resp) { + if (err) { + callback(err); + return; + } + var disk = new Disk(this, name, options.sizeGb); + callback(null, disk, resp); + }.bind(this)); +}; + +/** + * Get a list of disks in this zone. + * + * @throws {Error} if a malformed filter is provided. + * + * @param {object} options - Disk search options. + * @param {number=} options.maxResults - Maximum number of disks to return. + * @param {object=} options.filter - Search filter. + * @param {string} options.filter.fieldName - Disk field to consider in this + * filter. + * @param {string} options.filter.operatorString - Filter operator, can be + * either 'eq' or 'ne'. + * @param {string} options.filter.literalString - String to compare the disk + * field to. Can be a regular expression. + * @param {string=} pageToken - Page identifier used in paginated search. + * @param {function} callback - The callback function. + * + * @example + * var callback = function(err, disks) { + * // `disks` is an array of `Disk` objects. + * }; + * + * var compute = gcloud.compute({ + * projectId: 'grape-spaceship-123' + * }); + * + * var myZone = compute.zone('zone-name'); + * + * myZone.listDisks( + * { + * filter: { + * fieldName : 'name', + * operatorString : 'eq', + * literalString : 'disk-[0-9]' + * }], + * }, callback); + */ +Zone.prototype.listDisks = function(options, callback) { + if (!callback) { + callback = options; + } + + var query = {}; + + if (options) { + if (util.is(options.maxResults, 'number')) { + query.maxResults = options.maxResults; + } + if (options.filter) { + if (!util.is(options.filter.fieldName, 'string') || + !util.is(options.filter.operatorString, 'string') || + !util.is(options.filter.literalString, 'string')) { + throw new Error( + 'A filter must have name, comparisonString and literal fields.'); + } + if (options.filter.operatorString !== 'eq' && + options.filter.operatorString !== 'ne') { + throw new Error('Filter operator must be either eq or ne'); + } + query.filter = + options.filter.fieldName + ' ' + + options.filter.operatorString + ' \'' + + options.filter.literalString + '\''; + } + if (util.is(options.pageToken, 'string')) { + query.pageToken = options.pageToken; + } + } + + this.makeReq_( + 'GET', + '/disks', + query, + null, function(err, resp) { + if (err) { + callback(err); + return; + } + if (resp.items) { + var disks = []; + for (var i = 0; i < resp.items.length; i++) { + var disk = new Disk( + this, + resp.items[i].name, + resp.items[i].sizeGb, + resp.items[i]); + disks.push(disk); + } + callback(null, disks); + } else { + callback(null, []); + } + }.bind(this)); +}; + +/** + * Make a new request object from the provided arguments and wrap the callback + * to intercept non-successful responses. + * + * @private + * + * @param {string} method - Action. + * @param {string} path - Request path. + * @param {*} query - Request query object. + * @param {*} body - Request body contents. + * @param {function} callback - The callback function. + */ +Zone.prototype.makeReq_ = function(method, path, query, body, callback) { + var reqOpts = { + method: method, + qs: query, + uri: COMPUTE_BASE_URL + this.compute.projectId + + '/zones/' + this.name + path + }; + + if (body) { + reqOpts.json = body; + } + + this.compute.makeAuthorizedRequest_(reqOpts, callback); +}; + +module.exports = Zone; \ No newline at end of file From 35970b2fa0d706885fb9c72f21c17b367b1c35d8 Mon Sep 17 00:00:00 2001 From: Marco Ziccardi Date: Tue, 21 Jul 2015 13:04:30 +0200 Subject: [PATCH 3/9] Code style updates, linted code according to new rules, fixed minor bug on aggregated lists --- lib/compute/disk.js | 23 +---- lib/compute/firewall.js | 114 +++++++--------------- lib/compute/index.js | 206 ++++++++++++++++++++-------------------- lib/compute/instance.js | 53 ++++------- lib/compute/region.js | 22 +---- lib/compute/zone.js | 199 ++++++++++++++++++-------------------- 6 files changed, 254 insertions(+), 363 deletions(-) diff --git a/lib/compute/disk.js b/lib/compute/disk.js index a4ec1f118d9..846c96c2a44 100644 --- a/lib/compute/disk.js +++ b/lib/compute/disk.js @@ -26,12 +26,6 @@ */ var util = require('../common/util.js'); -/** - * @const {string} - * @private - */ -var COMPUTE_BASE_URL = 'https://www.googleapis.com/compute/v1/projects/'; - /** * Create a Disk object to interact with a Google Compute Engine disk. * @@ -40,7 +34,7 @@ var COMPUTE_BASE_URL = 'https://www.googleapis.com/compute/v1/projects/'; * * @throws {Error} if a disk name or a zone are not provided. * - * @param {module:zone} zone - The Google Compute Engine zone this + * @param {module:zone} zone - The Google Compute Engine zone this * disk belongs to. * @param {number=} sizeGb - Size of the disk in GB. * @param {object=} metadata - Disk metadata. @@ -128,19 +122,8 @@ Disk.prototype.createSnapshot = function(name, description, callback) { * @param {function} callback - The callback function. */ Disk.prototype.makeReq_ = function(method, path, query, body, callback) { - var reqOpts = { - method: method, - qs: query, - uri: COMPUTE_BASE_URL + this.zone.compute.projectId + - '/zones/' + this.zone.name + - '/disks/' + this.name + path - }; - - if (body) { - reqOpts.json = body; - } - - this.zone.compute.makeAuthorizedRequest_(reqOpts, callback); + path = '/disks/' + this.name + path; + this.zone.makeReq_(method, path, query, body, callback); }; module.exports = Disk; \ No newline at end of file diff --git a/lib/compute/firewall.js b/lib/compute/firewall.js index 0865e3bf125..e1558091668 100644 --- a/lib/compute/firewall.js +++ b/lib/compute/firewall.js @@ -20,18 +20,14 @@ 'use strict'; +var extend = require('extend'); + /** * @type {module:common/util} * @private */ var util = require('../common/util.js'); -/** - * @const {string} - * @private - */ -var COMPUTE_BASE_URL = 'https://www.googleapis.com/compute/v1/projects/'; - /** * Create a Firewall object to interact with a Google Compute Engine firewall. * @@ -40,19 +36,19 @@ var COMPUTE_BASE_URL = 'https://www.googleapis.com/compute/v1/projects/'; * * @throws {Error} if a firewall name isn't provided. * - * @param {module:compute} compute - The Google Compute Engine instance this + * @param {module:compute} compute - The Google Compute Engine instance this * firewall belongs to. * @param {string} name - Name of the firewall. * @param {object} options - Firewall options. * @param {string=} options.description - Description of the firewall. - * @param {string=} options.network - Network to which the firewall applies. + * @param {string=} options.network - Network to which the firewall applies. * Default value is 'global/networks/default'. * @param {object[]=} options.allowed - List of allowed protocols and ports. * @param {string[]=} options.sourceRanges - IP address blocks to which this * rule applies (in CIDR format). * @param {string[]=} options.sourceRanges - IP address blocks to which this * rule applies (in CIDR format). - * @param {string[]=} options.sourceTags - List of instance tags to which + * @param {string[]=} options.sourceTags - List of instance tags to which * this rule applies. * @param {string[]=} options.targetTags - List of instance tags indicating * instances that may process connections according to this rule. @@ -69,37 +65,18 @@ var COMPUTE_BASE_URL = 'https://www.googleapis.com/compute/v1/projects/'; function Firewall(compute, name, options) { this.name = name; this.compute = compute; - this.description = ''; - this.network = 'global/networks/default'; - this.allowed = []; - this.sourceRanges = []; - this.sourceTags = []; - this.targetTags = []; if (!this.name) { throw new Error('A name is needed to use Google Cloud Compute Firewall.'); } - if (options) { - if (util.is(options.description, 'string')) { - this.description = options.description; - } - if (util.is(options.network, 'string')) { - this.network = options.network; - } - if (util.is(options.allowed, 'array')) { - this.allowed = options.allowed; - } - if (util.is(options.sourceRanges, 'array')) { - this.sourceRanges = options.sourceRanges; - } - if (util.is(options.sourceTags, 'array')) { - this.sourceTags = options.sourceTags; - } - if (util.is(options.targetTags, 'array')) { - this.targetTags = options.targetTags; - } - } + options = options || {}; + this.description = options.description || ''; + this.network = options.network || 'global/networks/default'; + this.allowed = options.allowed || []; + this.sourceRanges = options.sourceRanges || []; + this.sourceTags = options.sourceTags || []; + this.targetTags = options.targetTags || []; } /** @@ -118,25 +95,25 @@ Firewall.prototype.delete = function(callback) { /** * Update the firewall rule. * - * @throws {Error} if firewall options are not provided, if no allowed - * protocols are specified or none of source ranges and source tags are + * @throws {Error} if firewall options are not provided, if no allowed + * protocols are specified or none of source ranges and source tags are * provided * - * @param {module:compute} compute - The Google Compute Engine instance this + * @param {module:compute} compute - The Google Compute Engine instance this * this firewall belongs to. * @param {string} name - Name of the firewall. * @param {object} options - Firewall options. * @param {boolean} options.patch - If true update is performed according to * the patch semantics (default is false). * @param {string=} options.description - Description of the firewall. - * @param {string=} options.network - Network to which the firewall applies. + * @param {string=} options.network - Network to which the firewall applies. * Default value is 'global/networks/default'. * @param {object[]=} options.allowed - List of allowed protocols and ports. * @param {string[]=} options.sourceRanges - IP address blocks to which this * rule applies (in CIDR format). * @param {string[]=} options.sourceRanges - IP address blocks to which this * rule applies (in CIDR format). - * @param {string[]=} options.sourceTags - List of instance tags to which + * @param {string[]=} options.sourceTags - List of instance tags to which * this rule applies. * @param {string[]=} options.targetTags - List of instance tags indicating * instances that may process connections according to this rule. @@ -154,7 +131,7 @@ Firewall.prototype.delete = function(callback) { * }, callback); */ Firewall.prototype.update = function(options, callback) { - if (!options) { + if (!util.is(options, 'object')) { throw new Error('Firewall rule options must be provided.'); } @@ -162,10 +139,10 @@ Firewall.prototype.update = function(options, callback) { if (options.patch) { method = 'PATCH'; } else { - if(!util.is(options.allowed, 'array')) { + if (!util.is(options.allowed, 'array')) { throw new Error('Allowed protocols and ports must be provided.'); } - if (!util.is(options.sourceRanges, 'array') && + if (!util.is(options.sourceRanges, 'array') && !util.is(options.sourceTags, 'array')) { throw new Error('Source ranges or source tags must be provided.'); } @@ -177,56 +154,43 @@ Firewall.prototype.update = function(options, callback) { } else { body.name = this.name; } - var newFirewall = this.compute.firewall(body.name); if (util.is(options.description, 'string')) { body.description = options.description; - newFirewall.description = body.description; - } else { - newFirewall.description = this.description; } + body.network = this.network; if (util.is(options.network, 'string')) { body.network = options.network; - newFirewall.network = body.network; - } else { - body.network = this.network; - newFirewall.network = this.network; } if (util.is(options.allowed, 'array')) { body.allowed = options.allowed; - newFirewall.allowed = body.allowed; - } else { - newFirewall.allowed = this.allowed; } if (util.is(options.sourceRanges, 'array')) { body.sourceRanges = options.sourceRanges; - newFirewall.sourceRanges = body.sourceRanges; - } else { - newFirewall.sourceRanges = this.sourceRanges; } if (util.is(options.sourceTags, 'array')) { body.sourceTags = options.sourceTags; - newFirewall.sourceTags = body.sourceTags; - } else { - newFirewall.sourceTags = this.sourceTags; } if (util.is(options.targetTags, 'array')) { body.targetTags = options.targetTags; - newFirewall.targetTags = body.targetTags; - } else { - newFirewall.targetTags = this.targetTags; } + var newOptions = body; + if (options.patch) { + newOptions = extend(this, body); + } + var newFirewall = new Firewall(this.compute, body.name, newOptions); + this.makeReq_( - method, - '', - null, + method, + '', + null, body, function(err, resp) { if (err) { callback(err); - return; + return; } callback(null, newFirewall, resp); - }.bind(this)); + }); }; /** @@ -242,18 +206,8 @@ Firewall.prototype.update = function(options, callback) { * @param {function} callback - The callback function. */ Firewall.prototype.makeReq_ = function(method, path, query, body, callback) { - var reqOpts = { - method: method, - qs: query, - uri: COMPUTE_BASE_URL + this.compute.projectId + - '/global/firewalls/' + this.name - }; - - if (body) { - reqOpts.json = body; - } - - this.compute.makeAuthorizedRequest_(reqOpts, callback); + path = '/global/firewalls/' + this.name; + this.compute.makeReq_(method, path, query, body, callback); }; module.exports = Firewall; \ No newline at end of file diff --git a/lib/compute/index.js b/lib/compute/index.js index 397fb6a1ef1..f3280bc6b0d 100644 --- a/lib/compute/index.js +++ b/lib/compute/index.js @@ -70,15 +70,15 @@ var SCOPES = ['https://www.googleapis.com/auth/compute']; var COMPUTE_BASE_URL = 'https://www.googleapis.com/compute/v1/projects/'; /** - * Create a Compute object to Interact with the Google Compute Engine API. - * Using this object, you can access your instances with - * {module:compute/instance}, disks with {module:compute/disk} and firewall + * Create a Compute object to Interact with the Google Compute Engine API. + * Using this object, you can access your instances with + * {module:compute/instance}, disks with {module:compute/disk} and firewall * rules with {module:compute/firewall}. * * Follow along with the examples to see how to do everything. With a Compute - * object it is possible to search for existing instances, disks and firewall - * as well as to create new ones. Instances can be stopped, reset and started - * and their tags can be updated. Disks can be deleted and attached to running + * object it is possible to search for existing instances, disks and firewall + * as well as to create new ones. Instances can be stopped, reset and started + * and their tags can be updated. Disks can be deleted and attached to running * instances. Firewall rules can be created and updated. * * @alias module:compute @@ -178,22 +178,22 @@ Compute.prototype.firewall = function(name, networkName) { /** * Create a firewall rule. * - * @throws {Error} if a firewall name or firewall options are not provided. + * @throws {Error} if a firewall name or firewall options are not provided. * If allowed ports, source tags or source ranges are not provided. * - * @param {module:compute} compute - The Google Compute Engine instance this + * @param {module:compute} compute - The Google Compute Engine instance this * this firewall belongs to. * @param {string} name - Name of the firewall. * @param {object} options - Firewall options. * @param {string=} options.description - Description of the firewall. - * @param {string=} options.network - Network to which the firewall applies. + * @param {string=} options.network - Network to which the firewall applies. * Default value is 'global/networks/default'. * @param {object[]=} options.allowed - List of allowed protocols and ports. * @param {string[]=} options.sourceRanges - IP address blocks to which this * rule applies (in CIDR format). * @param {string[]=} options.sourceRanges - IP address blocks to which this * rule applies (in CIDR format). - * @param {string[]=} options.sourceTags - List of instance tags to which + * @param {string[]=} options.sourceTags - List of instance tags to which * this rule applies. * @param {string[]=} options.targetTags - List of instance tags indicating * instances that may process connections according to this rule. @@ -208,7 +208,7 @@ Compute.prototype.firewall = function(name, networkName) { * projectId: 'grape-spaceship-123' * }); * - * var firewall = compute.createFirewall('tcp-3000', + * var firewall = compute.createFirewall('tcp-3000', * { * description: 'yada yada', * allowed: [{ @@ -228,7 +228,7 @@ Compute.prototype.createFirewall = function(name, options, callback) { if (!options || !util.is(options.allowed, 'array')) { throw new Error('Allowed protocols and ports must be provided.'); } - if (!util.is(options.sourceRanges, 'array') && + if (!util.is(options.sourceRanges, 'array') && !util.is(options.sourceTags, 'array')) { throw new Error('Source ranges or source tags must be provided.'); } @@ -245,29 +245,30 @@ Compute.prototype.createFirewall = function(name, options, callback) { } if (util.is(options.allowed, 'array')) { body.allowed = options.allowed; - } + } if (util.is(options.sourceRanges, 'array')) { body.sourceRanges = options.sourceRanges; - } + } if (util.is(options.sourceTags, 'array')) { body.sourceTags = options.sourceTags; - } + } if (util.is(options.targetTags, 'array')) { body.targetTags = options.targetTags; - } + } + var self = this; this.makeReq_( - 'POST', - '/global/firewalls', - null, + 'POST', + '/global/firewalls', + null, body, function(err, resp) { if (err) { callback(err); - return; + return; } - var firewall = new Firewall(this, name, body); + var firewall = new Firewall(self, name, body); callback(null, firewall, resp); - }.bind(this)); + }); }; /** @@ -276,7 +277,7 @@ Compute.prototype.createFirewall = function(name, options, callback) { * @throws {Error} if a malformed filter is provided. * * @param {object} options - Instance search options. - * @param {number=} options.maxResults - Maximum number of instances to + * @param {number=} options.maxResults - Maximum number of instances to * return. * @param {string=} options.zone - Only instances in this zone a returned. * @param {object=} options.filter - Search filter. @@ -298,7 +299,7 @@ Compute.prototype.createFirewall = function(name, options, callback) { * projectId: 'grape-spaceship-123' * }); * - * compute.listInstances( + * compute.listInstances( * { * zone: 'europe-west1-b', * filter: { @@ -309,66 +310,66 @@ Compute.prototype.createFirewall = function(name, options, callback) { * }, callback); */ Compute.prototype.listInstances = function(options, callback) { - if (!callback) { + if (util.is(options, 'function')) { callback = options; + options = {}; } var query = {}; - if (options) { - if (util.is(options.maxResults, 'number')) { - query.maxResults = options.maxResults; - } - if (options.filter) { - if (!util.is(options.filter.fieldName, 'string') || - !util.is(options.filter.operatorString, 'string') || - !util.is(options.filter.literalString, 'string')) { - throw new Error( - 'A filter must have name, comparisonString and literal fields.'); - } - if (options.filter.operatorString !== 'eq' && - options.filter.operatorString !== 'ne') { - throw new Error('Filter operator must be either eq or ne'); - } - query.filter = - options.filter.fieldName + ' ' + - options.filter.operatorString + ' \'' + - options.filter.literalString + '\''; + if (util.is(options.maxResults, 'number')) { + query.maxResults = options.maxResults; + } + if (options.filter) { + if (!util.is(options.filter.fieldName, 'string') || + !util.is(options.filter.operatorString, 'string') || + !util.is(options.filter.literalString, 'string')) { + throw new Error( + 'A filter must have name, comparisonString and literal fields.'); } - if (util.is(options.pageToken, 'string')) { - query.pageToken = options.pageToken; + if (options.filter.operatorString !== 'eq' && + options.filter.operatorString !== 'ne') { + throw new Error('Filter operator must be either eq or ne'); } + query.filter = + options.filter.fieldName + ' ' + + options.filter.operatorString + ' \'' + + options.filter.literalString + '\''; + } + if (util.is(options.pageToken, 'string')) { + query.pageToken = options.pageToken; } - + + var self = this; this.makeReq_( - 'GET', - '/aggregated/instances', - query, + 'GET', + '/aggregated/instances', + query, null, function(err, resp) { if (err) { callback(err); - return; + return; } if (resp.items) { var instances = []; for (var zoneName in resp.items) { - var zone = this.zone(zoneName); + var zone = self.zone(zoneName.replace('zones/', '')); var zoneInstances = resp.items[zoneName].instances; if (zoneInstances) { for (var i = 0; i < zoneInstances.length; i++) { var instance = new Instance( - zone, - zoneInstances[i].name, + zone, + zoneInstances[i].name, zoneInstances[i]); instances.push(instance); } } } - callback(null, instances); + callback(null, instances); } else { callback(null, []); } - }.bind(this)); + }); }; /** @@ -398,7 +399,7 @@ Compute.prototype.listInstances = function(options, callback) { * projectId: 'grape-spaceship-123' * }); * - * compute.listDisks( + * compute.listDisks( * { * zone: 'europe-west1-b', * filter: { @@ -409,67 +410,67 @@ Compute.prototype.listInstances = function(options, callback) { * }, callback); */ Compute.prototype.listDisks = function(options, callback) { - if (!callback) { + if (util.is(options, 'function')) { callback = options; + options = {}; } var query = {}; - if (options) { - if (util.is(options.maxResults, 'number')) { - query.maxResults = options.maxResults; - } - if (options.filter) { - if (!util.is(options.filter.fieldName, 'string') || - !util.is(options.filter.operatorString, 'string') || - !util.is(options.filter.literalString, 'string')) { - throw new Error( - 'A filter must have name, comparisonString and literal fields.'); - } - if (options.filter.operatorString !== 'eq' && - options.filter.operatorString !== 'ne') { - throw new Error('Filter operator must be either eq or ne'); - } - query.filter = - options.filter.fieldName + ' ' + - options.filter.operatorString + ' \'' + - options.filter.literalString + '\''; + if (util.is(options.maxResults, 'number')) { + query.maxResults = options.maxResults; + } + if (options.filter) { + if (!util.is(options.filter.fieldName, 'string') || + !util.is(options.filter.operatorString, 'string') || + !util.is(options.filter.literalString, 'string')) { + throw new Error( + 'A filter must have name, comparisonString and literal fields.'); } - if (util.is(options.pageToken, 'string')) { - query.pageToken = options.pageToken; + if (options.filter.operatorString !== 'eq' && + options.filter.operatorString !== 'ne') { + throw new Error('Filter operator must be either eq or ne'); } + query.filter = + options.filter.fieldName + ' ' + + options.filter.operatorString + ' \'' + + options.filter.literalString + '\''; } - + if (util.is(options.pageToken, 'string')) { + query.pageToken = options.pageToken; + } + + var self = this; this.makeReq_( - 'GET', - '/aggregated/disks', - query, + 'GET', + '/aggregated/disks', + query, null, function(err, resp) { if (err) { callback(err); - return; + return; } if (resp.items) { - var disks = []; + var disks = []; for (var zoneName in resp.items) { - var zone = this.zone(zoneName); + var zone = self.zone(zoneName.replace('zones/', '')); var zoneDisks = resp.items[zoneName].disks; if (zoneDisks) { for (var i = 0; i < zoneDisks.length; i++) { var disk = new Disk( - zone, - zoneDisks[i].name, - zoneDisks[i].sizeGb, + zone, + zoneDisks[i].name, + zoneDisks[i].sizeGb, zoneDisks[i]); disks.push(disk); } } } - callback(null, disks); + callback(null, disks); } else { callback(null, []); } - }.bind(this)); + }); }; /** @@ -498,7 +499,7 @@ Compute.prototype.listDisks = function(options, callback) { * projectId: 'grape-spaceship-123' * }); * - * compute.listFirewalls( + * compute.listFirewalls( * { * zone: 'europe-west1-b', * filter: { @@ -525,11 +526,11 @@ Compute.prototype.listFirewalls = function(options, callback) { throw new Error( 'A filter must have name, comparisonString and literal fields.'); } - if (options.filter.operatorString !== 'eq' && + if (options.filter.operatorString !== 'eq' && options.filter.operatorString !== 'ne') { throw new Error('Filter operator must be either eq or ne'); } - query.filter = + query.filter = options.filter.fieldName + ' ' + options.filter.operatorString + ' \'' + options.filter.literalString + '\''; @@ -538,21 +539,22 @@ Compute.prototype.listFirewalls = function(options, callback) { query.pageToken = options.pageToken; } + var self = this; this.makeReq_( - 'GET', - '/global/firewalls', - query, + 'GET', + '/global/firewalls', + query, null, function(err, resp) { if (err) { callback(err); - return; + return; } if (resp.items) { var firewalls = []; for (var i = 0; i < resp.items.length; i++) { var firewall = new Firewall( - this, - resp.items[i].name, + self, + resp.items[i].name, { description: resp.items[i].description, network: resp.items[i].network, @@ -563,11 +565,11 @@ Compute.prototype.listFirewalls = function(options, callback) { }); firewalls.push(firewall); } - callback(null, firewalls); + callback(null, firewalls); } else { callback(null, []); } - }.bind(this)); + }); }; /** diff --git a/lib/compute/instance.js b/lib/compute/instance.js index d55ad6d4ea7..f221dd5d8b0 100644 --- a/lib/compute/instance.js +++ b/lib/compute/instance.js @@ -26,12 +26,6 @@ */ var util = require('../common/util.js'); -/** - * @const {string} - * @private - */ -var COMPUTE_BASE_URL = 'https://www.googleapis.com/compute/v1/projects/'; - /** * Create a Instance object to interact with a Google Compute Engine instance. * @@ -40,7 +34,7 @@ var COMPUTE_BASE_URL = 'https://www.googleapis.com/compute/v1/projects/'; * * @throws {Error} if an instance name or a zone are not provided. * - * @param {module:zone} zone - The Google Compute Engine Zone this + * @param {module:zone} zone - The Google Compute Engine Zone this * instance belongs to. * @param {string} name - Name of the instance. * @param {object=} metadata - Instance metadata. @@ -90,13 +84,13 @@ Instance.prototype.delete = function(callback) { * @param {module:compute/disk} disk - The disk to attach. * @param {object=} options - Disk attach options. * @param {boolean=} otpions.boot - If true the disk is attached as a boot disk. - * @param {boolean=} options.readOnly - If true the disk is attached as + * @param {boolean=} options.readOnly - If true the disk is attached as * `READ_ONLY`. * @param {number=} options.index - Zero based index assigned from the instance * to the disk. - * @param {string=} options.deviceName - Device name assigned from the instance + * @param {string=} options.deviceName - Device name assigned from the instance * to the disk. - * @param {boolean=} options.autoDelete - If true the disk is deleted when the + * @param {boolean=} options.autoDelete - If true the disk is deleted when the * instance is deleted. * @param {function} callback - The callback function. * @@ -196,16 +190,16 @@ Instance.prototype.getSerialPortOutput = function(port, callback) { query.port = port; this.makeReq_( - 'GET', - '/serialPort', - query, + 'GET', + '/serialPort', + query, null, function(err, resp) { if (err) { callback(err); - return; + return; } callback(null, resp.content); - }.bind(this)); + }); }; /** @@ -262,19 +256,19 @@ Instance.prototype.getTags = function(callback) { this.makeReq_('GET', '', null, null, function(err, resp) { if (err) { callback(err); - return; + return; } this.metadata = resp; - callback(null, resp.tags.items, resp.tags.fingerprint); - }.bind(this)); + callback(null, resp.tags.items, resp.tags.fingerprint); + }); }; /** * Set instance's tags. * * @param {{string[]} tags - The new tags for the instance. - * @param {string} fingerprint - The current tags fingerprint. Up-to-date - * fingerprint must be provided to set tags. + * @param {string} fingerprint - The current tags fingerprint. Up-to-date + * fingerprint must be provided to set tags. * @param {function} callback - The callback function. * * @example @@ -292,12 +286,12 @@ Instance.prototype.setTags = function(tags, fingerprint, callback) { if (!util.is(fingerprint, 'string')) { throw new Error('You must provide a fingerprint.'); } - for(var i = 0; i < tags.length; i++) { + for (var i = 0; i < tags.length; i++) { if (!/^(?:[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?)$/.test(tags[i])) { throw new Error('Tags must match [a-z]([-a-z0-9]{0,61}[a-z0-9])?'); } } - + var body = {}; body.items = tags; body.fingerprint = fingerprint; @@ -318,19 +312,8 @@ Instance.prototype.setTags = function(tags, fingerprint, callback) { * @param {function} callback - The callback function. */ Instance.prototype.makeReq_ = function(method, path, query, body, callback) { - var reqOpts = { - method: method, - qs: query, - uri: COMPUTE_BASE_URL + this.zone.compute.projectId + - '/zones/' + this.zone.name + - '/instances/' + this.name + path - }; - - if (body) { - reqOpts.json = body; - } - - this.zone.compute.makeAuthorizedRequest_(reqOpts, callback); + path = '/instances/' + this.name + path; + this.zone.makeReq_(method, path, query, body, callback); }; module.exports = Instance; diff --git a/lib/compute/region.js b/lib/compute/region.js index 1beeec847c3..83298399b48 100644 --- a/lib/compute/region.js +++ b/lib/compute/region.js @@ -20,12 +20,6 @@ 'use strict'; -/** - * @const {string} - * @private - */ -var COMPUTE_BASE_URL = 'https://www.googleapis.com/compute/v1/projects/'; - /** * Create a Region object to interact with a Google Compute Engine region. * @@ -34,7 +28,7 @@ var COMPUTE_BASE_URL = 'https://www.googleapis.com/compute/v1/projects/'; * * @throws {Error} if a region name is not provided. * - * @param {module:compute} compute - The Google Compute Engine instance this + * @param {module:compute} compute - The Google Compute Engine instance this * disk belongs to. * @param {string} name - Name of the region. * @@ -69,18 +63,8 @@ function Region(compute, name) { * @param {function} callback - The callback function. */ Region.prototype.makeReq_ = function(method, path, query, body, callback) { - var reqOpts = { - method: method, - qs: query, - uri: COMPUTE_BASE_URL + this.compute.projectId + - '/regions/' + this.name + path - }; - - if (body) { - reqOpts.json = body; - } - - this.compute.makeAuthorizedRequest_(reqOpts, callback); + path = '/regions/' + this.name + path; + this.compute.makeReq_(method, path, query, body, callback); }; module.exports = Region; \ No newline at end of file diff --git a/lib/compute/zone.js b/lib/compute/zone.js index 49d12ea7795..f406efeb0f8 100644 --- a/lib/compute/zone.js +++ b/lib/compute/zone.js @@ -38,12 +38,6 @@ var Disk = require('./disk.js'); */ var util = require('../common/util.js'); -/** - * @const {string} - * @private - */ -var COMPUTE_BASE_URL = 'https://www.googleapis.com/compute/v1/projects/'; - /** * Create a Zone object to interact with a Google Compute Engine zone. * @@ -52,7 +46,7 @@ var COMPUTE_BASE_URL = 'https://www.googleapis.com/compute/v1/projects/'; * * @throws {Error} if a zone name is not provided. * - * @param {module:compute} compute - The Google Compute Engine instance this + * @param {module:compute} compute - The Google Compute Engine instance this * disk belongs to. * @param {string} name - Name of the zone. * @@ -125,20 +119,20 @@ Zone.prototype.disk = function(name) { * @param {object} options - Options for instance creation. * @param {string} machineType - Type of the instance. * @param {object[]} disks - Disks to attach to the instance. - * @param {boolean} disks[].createNew - True if the disk has to be created. + * @param {boolean} disks[].createNew - True if the disk has to be created. * Default value is false. * @param {string} disks[].source - Source for the disk to be created, either an - * image or a snapshot. + * image or a snapshot. * @param {string=} disks[].diskType - Type of the disk. * @param {number=} disks[].sizeGb - Size of the disk in GB. * @param {boolean=} disks[].boot - True of the disk is a boot disk. - * @param {string=} disks[].mode - Attach mode, either `READ_ONLY` or + * @param {string=} disks[].mode - Attach mode, either `READ_ONLY` or * `READ_WRITE`. Default value is `READ_WRITE`. * @param {number=} disks[].index - Zero based index assigned from the instance * to the disk. - * @param {string=} disks[].deviceName - Device name assigned from the instance + * @param {string=} disks[].deviceName - Device name assigned from the instance * to the disk. - * @param {boolean=} disks[].autoDelete - If true the disk is deleted when the + * @param {boolean=} disks[].autoDelete - If true the disk is deleted when the * instance is deleted. * @param {object[]} networkInterfaces - Network interfaces for the instance. * @param {string} networkInterfaces[].network - Full/partial URL of a network @@ -157,13 +151,13 @@ Zone.prototype.disk = function(name) { * var myZone = compute.zone('zone-name'); * * myZone.createInstance( - * 'instance-1', + * 'instance-1', * { * machineType: 'zones/europe-west1-b/machineTypes/n1-standard-1', * disks: [{ * createNew: true, * diskName: 'test-create-disk', - * source: + * source: * 'projects/debian-cloud/global/images/debian-7-wheezy-v20150325', * diskSizeGb: 10, * boot: true, @@ -201,7 +195,7 @@ Zone.prototype.createInstance = function(name, options, callback) { if (!util.is(options.disks, 'array')) { options.disks = [options.disks]; - } + } options.disks.forEach(function(disk) { var requestDisk = {}; @@ -255,20 +249,21 @@ Zone.prototype.createInstance = function(name, options, callback) { body.networkInterfaces.push({ network: networkInterface.network }); - }); + }); + var self = this; this.makeReq_( - 'POST', - '/instances', - query, + 'POST', + '/instances', + query, body, function(err, resp) { if (err) { callback(err); - return; + return; } - var instance = new Instance(this, name); + var instance = new Instance(self, name); callback(null, instance, resp); - }.bind(this)); + }); }; /** @@ -277,7 +272,7 @@ Zone.prototype.createInstance = function(name, options, callback) { * @throws {Error} if a malformed filter is provided. * * @param {object} options - Instance search options. - * @param {number=} options.maxResults - Maximum number of instances to + * @param {number=} options.maxResults - Maximum number of instances to * return. * @param {object=} options.filter - Search filter. * @param {string} options.filter.fieldName - Instance field to consider in this @@ -300,7 +295,7 @@ Zone.prototype.createInstance = function(name, options, callback) { * * var myZone = compute.zone('zone-name'); * - * myZone.listInstances( + * myZone.listInstances( * { * filter: { * fieldName : 'name', @@ -310,61 +305,60 @@ Zone.prototype.createInstance = function(name, options, callback) { * }, callback); */ Zone.prototype.listInstances = function(options, callback) { - if (!callback) { + if (util.is(options, 'function')) { callback = options; + options = {}; } var query = {}; - if (options) { - if (util.is(options.maxResults, 'number')) { - query.maxResults = options.maxResults; - } - if (options.filter) { - if (!util.is(options.filter.fieldName, 'string') || - !util.is(options.filter.operatorString, 'string') || - !util.is(options.filter.literalString, 'string')) { - throw new Error( - 'A filter must have name, comparisonString and literal fields.'); - } - if (options.filter.operatorString !== 'eq' && - options.filter.operatorString !== 'ne') { - throw new Error('Filter operator must be either eq or ne'); - } - query.filter = - options.filter.fieldName + ' ' + - options.filter.operatorString + ' \'' + - options.filter.literalString + '\''; + if (util.is(options.maxResults, 'number')) { + query.maxResults = options.maxResults; + } + if (options.filter) { + if (!util.is(options.filter.fieldName, 'string') || + !util.is(options.filter.operatorString, 'string') || + !util.is(options.filter.literalString, 'string')) { + throw new Error( + 'A filter must have name, comparisonString and literal fields.'); } - if (util.is(options.pageToken, 'string')) { - query.pageToken = options.pageToken; + if (options.filter.operatorString !== 'eq' && + options.filter.operatorString !== 'ne') { + throw new Error('Filter operator must be either eq or ne'); } + query.filter = + options.filter.fieldName + ' ' + + options.filter.operatorString + ' \'' + + options.filter.literalString + '\''; + } + if (util.is(options.pageToken, 'string')) { + query.pageToken = options.pageToken; } + var self = this; this.makeReq_( - 'GET', - '/instances', - query, + 'GET', + '/instances', + query, null, function(err, resp) { if (err) { callback(err); - return; + return; } - console.log(resp); if (resp.items) { var instances = []; for (var i = 0; i < resp.items.length; i++) { var instance = new Instance( - this, - resp.items[i].name, + self, + resp.items[i].name, resp.items[i]); instances.push(instance); } - callback(null, instances); + callback(null, instances); } else { callback(null, []); } - }.bind(this)); + }); }; /** @@ -392,7 +386,7 @@ Zone.prototype.listInstances = function(options, callback) { * var myZone = compute.zone('zone-name'); * * myZone.createDisk( - * 'new-disk', + * 'new-disk', * { * sizeGb: 10 * }, callback); @@ -431,18 +425,19 @@ Zone.prototype.createDisk = function(name, options, callback) { body.name = name; + var self = this; this.makeReq_( - 'POST', - '/disks', - query, + 'POST', + '/disks', + query, body, function(err, resp) { if (err) { callback(err); - return; + return; } - var disk = new Disk(this, name, options.sizeGb); + var disk = new Disk(self, name, options.sizeGb); callback(null, disk, resp); - }.bind(this)); + }); }; /** @@ -473,7 +468,7 @@ Zone.prototype.createDisk = function(name, options, callback) { * * var myZone = compute.zone('zone-name'); * - * myZone.listDisks( + * myZone.listDisks( * { * filter: { * fieldName : 'name', @@ -483,61 +478,61 @@ Zone.prototype.createDisk = function(name, options, callback) { * }, callback); */ Zone.prototype.listDisks = function(options, callback) { - if (!callback) { + if (util.is(options, 'function')) { callback = options; + options = {}; } var query = {}; - if (options) { - if (util.is(options.maxResults, 'number')) { - query.maxResults = options.maxResults; - } - if (options.filter) { - if (!util.is(options.filter.fieldName, 'string') || - !util.is(options.filter.operatorString, 'string') || - !util.is(options.filter.literalString, 'string')) { - throw new Error( - 'A filter must have name, comparisonString and literal fields.'); - } - if (options.filter.operatorString !== 'eq' && - options.filter.operatorString !== 'ne') { - throw new Error('Filter operator must be either eq or ne'); - } - query.filter = - options.filter.fieldName + ' ' + - options.filter.operatorString + ' \'' + - options.filter.literalString + '\''; + if (util.is(options.maxResults, 'number')) { + query.maxResults = options.maxResults; + } + if (options.filter) { + if (!util.is(options.filter.fieldName, 'string') || + !util.is(options.filter.operatorString, 'string') || + !util.is(options.filter.literalString, 'string')) { + throw new Error( + 'A filter must have name, comparisonString and literal fields.'); } - if (util.is(options.pageToken, 'string')) { - query.pageToken = options.pageToken; + if (options.filter.operatorString !== 'eq' && + options.filter.operatorString !== 'ne') { + throw new Error('Filter operator must be either eq or ne'); } + query.filter = + options.filter.fieldName + ' ' + + options.filter.operatorString + ' \'' + + options.filter.literalString + '\''; + } + if (util.is(options.pageToken, 'string')) { + query.pageToken = options.pageToken; } - + + var self = this; this.makeReq_( - 'GET', - '/disks', - query, + 'GET', + '/disks', + query, null, function(err, resp) { if (err) { callback(err); - return; + return; } if (resp.items) { var disks = []; for (var i = 0; i < resp.items.length; i++) { var disk = new Disk( - this, - resp.items[i].name, - resp.items[i].sizeGb, + self, + resp.items[i].name, + resp.items[i].sizeGb, resp.items[i]); disks.push(disk); } - callback(null, disks); + callback(null, disks); } else { callback(null, []); } - }.bind(this)); + }); }; /** @@ -553,18 +548,8 @@ Zone.prototype.listDisks = function(options, callback) { * @param {function} callback - The callback function. */ Zone.prototype.makeReq_ = function(method, path, query, body, callback) { - var reqOpts = { - method: method, - qs: query, - uri: COMPUTE_BASE_URL + this.compute.projectId + - '/zones/' + this.name + path - }; - - if (body) { - reqOpts.json = body; - } - - this.compute.makeAuthorizedRequest_(reqOpts, callback); + path = '/zones/' + this.name + path; + this.compute.makeReq_(method, path, query, body, callback); }; module.exports = Zone; \ No newline at end of file From 3522baca395d61f20f466daac46f5d5196d61ccb Mon Sep 17 00:00:00 2001 From: Marco Ziccardi Date: Wed, 22 Jul 2015 12:49:32 +0200 Subject: [PATCH 4/9] Rename list methods, add Snapshot class, add getMetadata methods, removed update in favor of setMetadata for Firewall class, other minor refactoring and docs update --- lib/compute/disk.js | 72 ++++++++++------ lib/compute/firewall.js | 145 ++++++++++++--------------------- lib/compute/index.js | 151 ++++++++++++++++++++++++++++------ lib/compute/instance.js | 25 +++++- lib/compute/region.js | 2 +- lib/compute/snapshot.js | 112 +++++++++++++++++++++++++ lib/compute/zone.js | 176 ++++++++++++++++++---------------------- 7 files changed, 446 insertions(+), 237 deletions(-) create mode 100644 lib/compute/snapshot.js diff --git a/lib/compute/disk.js b/lib/compute/disk.js index 846c96c2a44..983cad8ffc9 100644 --- a/lib/compute/disk.js +++ b/lib/compute/disk.js @@ -36,7 +36,7 @@ var util = require('../common/util.js'); * * @param {module:zone} zone - The Google Compute Engine zone this * disk belongs to. - * @param {number=} sizeGb - Size of the disk in GB. + * @param {string} name - Disk name. * @param {object=} metadata - Disk metadata. * * @example @@ -50,19 +50,17 @@ var util = require('../common/util.js'); * * var disk = myZone.disk('disk1'); */ -function Disk(zone, name, sizeGb, metadata) { +function Disk(zone, name, metadata) { this.name = name; this.zone = zone; + this.metadata = metadata; if (!util.is(this.name, 'string')) { - throw new Error('A name is needed to use Google Cloud Compute Disk.'); + throw new Error('A name is needed to use a Compute Engine Disk.'); } if (!this.zone) { - throw new Error('A zone is needed to use Google Cloud Compute Disk.'); + throw new Error('A zone is needed to use a Compute Engine Disk.'); } - - this.sizeGb = sizeGb; - this.metadata = metadata; } /** @@ -78,35 +76,61 @@ Disk.prototype.delete = function(callback) { this.makeReq_('DELETE', '', null, true, callback); }; +/** + * Get the disk's metadata. + * + * @param {function=} callback - The callback function. + * + * @example + * disk.getMetadata(function(err, metadata, apiResponse) {}); + */ +Disk.prototype.getMetadata = function(callback) { + callback = callback || util.noop; + var self = this; + this.makeReq_('GET', '', null, null, function(err, resp) { + if (err) { + callback(err); + return; + } + self.metadata = resp; + callback(null, self.metadata, resp); + }); +}; + /** * Create a snapshot of a disk. * - * @param {string} name - Name of the snapshot. - * @param {string=} description - Description of the snapshot. + * @param {string|object} options - Snapshot options or snapshot name. + * @param {string} options.name - Name of the snapshot. + * @param {string=} options.description - Description of the snapshot. * @param {function} callback - The callback function. * * @example - * disk.createSnapshot('my-snapshot', function(err, apiResponse) { - * // Handle error + * disk.createSnapshot('my-snapshot', function(err, snapshot, apiResponse) { + * // `snapshot` is a Snapshot object. * }); */ -Disk.prototype.createSnapshot = function(name, description, callback) { - if (!name) { +Disk.prototype.createSnapshot = function(options, callback) { + if (util.is(options, 'string')) { + options = { + name: options + }; + } + if (!options.name) { throw new Error('A name is required to create a snapshot.'); - } else if (!/^(?:[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?)$/.test(name)) { + } else if (!/^(?:[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?)$/.test(options.name)) { throw new Error('Name must match [a-z]([-a-z0-9]{0,61}[a-z0-9])?'); } - if (!callback) { - callback = description; - } - - var body = {}; - body.name = name; - if (description) { - body.description = description; - } - this.makeReq_('POST', '/createSnapshot', null, body, callback); + var compute = this.zone.compute; + this.makeReq_('POST', '/createSnapshot', null, options, function(err, resp) { + if (err) { + callback(err); + return; + } + var snapshot = compute.snapshot(options.name); + callback(null, snapshot, resp); + }); }; /** diff --git a/lib/compute/firewall.js b/lib/compute/firewall.js index e1558091668..a11b1d3a568 100644 --- a/lib/compute/firewall.js +++ b/lib/compute/firewall.js @@ -20,8 +20,6 @@ 'use strict'; -var extend = require('extend'); - /** * @type {module:common/util} * @private @@ -36,8 +34,8 @@ var util = require('../common/util.js'); * * @throws {Error} if a firewall name isn't provided. * - * @param {module:compute} compute - The Google Compute Engine instance this - * firewall belongs to. + * @param {module:compute} compute - The Compute module instance this firewall + * belongs to. * @param {string} name - Name of the firewall. * @param {object} options - Firewall options. * @param {string=} options.description - Description of the firewall. @@ -63,20 +61,14 @@ var util = require('../common/util.js'); * var firewall = compute.firewall('tcp-3000'); */ function Firewall(compute, name, options) { - this.name = name; this.compute = compute; + this.name = name; + this.metadata = options || {}; + this.metadata.network = this.metadata.network || 'global/networks/default'; if (!this.name) { - throw new Error('A name is needed to use Google Cloud Compute Firewall.'); + throw new Error('A name is needed to use a Compute Engine Firewall.'); } - - options = options || {}; - this.description = options.description || ''; - this.network = options.network || 'global/networks/default'; - this.allowed = options.allowed || []; - this.sourceRanges = options.sourceRanges || []; - this.sourceTags = options.sourceTags || []; - this.targetTags = options.targetTags || []; } /** @@ -93,104 +85,75 @@ Firewall.prototype.delete = function(callback) { }; /** - * Update the firewall rule. + * Get the firewalls's metadata. * - * @throws {Error} if firewall options are not provided, if no allowed - * protocols are specified or none of source ranges and source tags are - * provided + * @param {function=} callback - The callback function. + * + * @example + * firewall.getMetadata(function(err, metadata, apiResponse) {}); + */ +Firewall.prototype.getMetadata = function(callback) { + callback = callback || util.noop; + var self = this; + this.makeReq_('GET', '', null, null, function(err, resp) { + if (err) { + callback(err); + return; + } + self.metadata = resp; + callback(null, self.metadata, resp); + }); +}; + +/** + * Set the firewall's metadata. + * + * @throws {Error} if firewall options are not provided. * * @param {module:compute} compute - The Google Compute Engine instance this * this firewall belongs to. - * @param {string} name - Name of the firewall. - * @param {object} options - Firewall options. - * @param {boolean} options.patch - If true update is performed according to - * the patch semantics (default is false). - * @param {string=} options.description - Description of the firewall. - * @param {string=} options.network - Network to which the firewall applies. + * @param {object} metadata - Firewall options. + * @param {string=} metadata.network - Network to which the firewall applies. * Default value is 'global/networks/default'. - * @param {object[]=} options.allowed - List of allowed protocols and ports. - * @param {string[]=} options.sourceRanges - IP address blocks to which this + * @param {object[]=} metadata.allowed - List of allowed protocols and ports. + * @param {string[]=} metadata.sourceRanges - IP address blocks to which this * rule applies (in CIDR format). - * @param {string[]=} options.sourceRanges - IP address blocks to which this + * @param {string[]=} metadata.sourceRanges - IP address blocks to which this * rule applies (in CIDR format). - * @param {string[]=} options.sourceTags - List of instance tags to which + * @param {string[]=} metadata.sourceTags - List of instance tags to which * this rule applies. - * @param {string[]=} options.targetTags - List of instance tags indicating + * @param {string[]=} metadata.targetTags - List of instance tags indicating * instances that may process connections according to this rule. * @param {function} callback - The callback function. * * @example - * var callback = function(err, firewall, apiResponse) { - * // `firewall` is a Firewall object. + * var callback = function(err, apiResponse) { + * // ... * }; * - * firewall.update( + * firewall.setMetadata( * { - * patch: true, * description: 'Brand new description', * }, callback); */ -Firewall.prototype.update = function(options, callback) { - if (!util.is(options, 'object')) { +Firewall.prototype.setMetadata = function(metadata, callback) { + callback = callback || util.noop; + if (!util.is(metadata, 'object')) { throw new Error('Firewall rule options must be provided.'); } - var method = 'PUT'; - if (options.patch) { - method = 'PATCH'; - } else { - if (!util.is(options.allowed, 'array')) { - throw new Error('Allowed protocols and ports must be provided.'); - } - if (!util.is(options.sourceRanges, 'array') && - !util.is(options.sourceTags, 'array')) { - throw new Error('Source ranges or source tags must be provided.'); - } - } + metadata.name = this.name; + metadata.network = this.metadata.network; - var body = {}; - if (util.is(options.name, 'string')) { - body.name = options.name; - } else { - body.name = this.name; - } - if (util.is(options.description, 'string')) { - body.description = options.description; - } - body.network = this.network; - if (util.is(options.network, 'string')) { - body.network = options.network; - } - if (util.is(options.allowed, 'array')) { - body.allowed = options.allowed; - } - if (util.is(options.sourceRanges, 'array')) { - body.sourceRanges = options.sourceRanges; - } - if (util.is(options.sourceTags, 'array')) { - body.sourceTags = options.sourceTags; - } - if (util.is(options.targetTags, 'array')) { - body.targetTags = options.targetTags; - } - - var newOptions = body; - if (options.patch) { - newOptions = extend(this, body); - } - var newFirewall = new Firewall(this.compute, body.name, newOptions); - - this.makeReq_( - method, - '', - null, - body, function(err, resp) { - if (err) { - callback(err); - return; - } - callback(null, newFirewall, resp); - }); + var self = this; + this.makeReq_('PATCH', '', null, metadata, function(err, resp) { + if (err) { + callback(err); + return; + } + self.metadata = metadata; + callback(null, resp); + }); }; /** @@ -206,7 +169,7 @@ Firewall.prototype.update = function(options, callback) { * @param {function} callback - The callback function. */ Firewall.prototype.makeReq_ = function(method, path, query, body, callback) { - path = '/global/firewalls/' + this.name; + path = '/global/firewalls/' + this.name + path; this.compute.makeReq_(method, path, query, body, callback); }; diff --git a/lib/compute/index.js b/lib/compute/index.js index f3280bc6b0d..944d1a499a5 100644 --- a/lib/compute/index.js +++ b/lib/compute/index.js @@ -50,6 +50,12 @@ var Disk = require('./disk.js'); */ var Firewall = require('./firewall.js'); +/** + * @type {module:compute/snapshot} + * @private + */ +var Snapshot = require('./snapshot.js'); + /** * @type {module:common/util} * @private @@ -167,12 +173,28 @@ Compute.prototype.zone = function(name) { * var firewallRule = compute.firewall('rule1'); */ Compute.prototype.firewall = function(name, networkName) { - if (networkName) { - return new Firewall(this, networkName, { - network: networkName - }); - } - return new Firewall(this, name); + var options = {}; + options.networkName = networkName; + return new Firewall(this, name, options); +}; + +/** + * Get a reference to a Google Compute Engine snapshot. + * + * @param {string} name - Name of the existing snapshot. + * @return {module:compute/snapshot} + * + * @example + * var gcloud = require('gcloud')({ + * keyFilename: '/path/to/keyfile.json' + * }); + * + * var compute = gcloud.compute(); + * + * var snapshot = compute.snapshot('snapshot-name'); + */ +Compute.prototype.snapshot = function(name) { + return new Snapshot(this, name); }; /** @@ -221,7 +243,7 @@ Compute.prototype.firewall = function(name, networkName) { */ Compute.prototype.createFirewall = function(name, options, callback) { if (!name) { - throw new Error('A firewall name is needed to use Google Cloud Compute.'); + throw new Error('A name is needed to use a Compute Engine Firewall.'); } else if (!/^(?:[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?)$/.test(name)) { throw new Error('Name must match [a-z]([-a-z0-9]{0,61}[a-z0-9])?'); } @@ -266,7 +288,8 @@ Compute.prototype.createFirewall = function(name, options, callback) { callback(err); return; } - var firewall = new Firewall(self, name, body); + var firewall = self.firewall(name); + firewall.name = options; callback(null, firewall, resp); }); }; @@ -279,7 +302,6 @@ Compute.prototype.createFirewall = function(name, options, callback) { * @param {object} options - Instance search options. * @param {number=} options.maxResults - Maximum number of instances to * return. - * @param {string=} options.zone - Only instances in this zone a returned. * @param {object=} options.filter - Search filter. * @param {string} options.filter.fieldName - Instance field to consider in this * filter. @@ -299,9 +321,8 @@ Compute.prototype.createFirewall = function(name, options, callback) { * projectId: 'grape-spaceship-123' * }); * - * compute.listInstances( + * compute.getInstances( * { - * zone: 'europe-west1-b', * filter: { * fieldName : 'name', * operatorString : 'eq', @@ -309,7 +330,7 @@ Compute.prototype.createFirewall = function(name, options, callback) { * }], * }, callback); */ -Compute.prototype.listInstances = function(options, callback) { +Compute.prototype.getInstances = function(options, callback) { if (util.is(options, 'function')) { callback = options; options = {}; @@ -379,7 +400,6 @@ Compute.prototype.listInstances = function(options, callback) { * * @param {object} options - Disk search options. * @param {number=} options.maxResults - Maximum number of disks to return. - * @param {string=} options.zone - Only disks in this zone a returned. * @param {object=} options.filter - Search filter. * @param {string} options.filter.fieldName - Disk field to consider in this * filter. @@ -399,9 +419,8 @@ Compute.prototype.listInstances = function(options, callback) { * projectId: 'grape-spaceship-123' * }); * - * compute.listDisks( + * compute.getDisks( * { - * zone: 'europe-west1-b', * filter: { * fieldName : 'name', * operatorString : 'eq', @@ -409,7 +428,7 @@ Compute.prototype.listInstances = function(options, callback) { * }], * }, callback); */ -Compute.prototype.listDisks = function(options, callback) { +Compute.prototype.getDisks = function(options, callback) { if (util.is(options, 'function')) { callback = options; options = {}; @@ -457,11 +476,7 @@ Compute.prototype.listDisks = function(options, callback) { var zoneDisks = resp.items[zoneName].disks; if (zoneDisks) { for (var i = 0; i < zoneDisks.length; i++) { - var disk = new Disk( - zone, - zoneDisks[i].name, - zoneDisks[i].sizeGb, - zoneDisks[i]); + var disk = new Disk(zone, zoneDisks[i].name, zoneDisks[i]); disks.push(disk); } } @@ -473,6 +488,96 @@ Compute.prototype.listDisks = function(options, callback) { }); }; +/** + * Get a list of snapshots. + * + * @throws {Error} if a malformed filter is provided. + * + * @param {object} options - Disk search options. + * @param {number=} options.maxResults - Maximum number of snapshots to return. + * @param {object=} options.filter - Search filter. + * @param {string} options.filter.fieldName - Snapshot field to consider in this + * filter. + * @param {string} options.filter.operatorString - Filter operator, can be + * either 'eq' or 'ne'. + * @param {string} options.filter.literalString - String to compare the snapshot + * field to. Can be a regular expression. + * @param {string=} pageToken - Page identifier used in paginated search. + * @param {function} callback - The callback function. + * + * @example + * var callback = function(err, snapshots) { + * // `disks` is an array of `Snapshot` objects. + * }; + * + * var compute = gcloud.compute({ + * projectId: 'grape-spaceship-123' + * }); + * + * compute.getSnapshots( + * { + * filter: { + * fieldName : 'name', + * operatorString : 'eq', + * literalString : 'snapshot-[0-9]' + * }], + * }, callback); + */ +Compute.prototype.getSnapshots = function(options, callback) { + if (util.is(options, 'function')) { + callback = options; + options = {}; + } + + var query = {}; + + if (util.is(options.maxResults, 'number')) { + query.maxResults = options.maxResults; + } + if (options.filter) { + if (!util.is(options.filter.fieldName, 'string') || + !util.is(options.filter.operatorString, 'string') || + !util.is(options.filter.literalString, 'string')) { + throw new Error( + 'A filter must have name, comparisonString and literal fields.'); + } + if (options.filter.operatorString !== 'eq' && + options.filter.operatorString !== 'ne') { + throw new Error('Filter operator must be either eq or ne'); + } + query.filter = + options.filter.fieldName + ' ' + + options.filter.operatorString + ' \'' + + options.filter.literalString + '\''; + } + if (util.is(options.pageToken, 'string')) { + query.pageToken = options.pageToken; + } + + var self = this; + this.makeReq_('GET', + '/global/snapshots', + query, + null, function(err, resp) { + if (err) { + callback(err); + return; + } + if (resp.items) { + var snapshots = []; + for (var i = 0; i < resp.items.length; i++) { + var metadata = resp.items[i]; + var snapshot = self.snapshot(metadata.name); + snapshot.metadata = metadata; + snapshots.push(snapshot); + } + callback(null, snapshots); + } else { + callback(null, []); + } + }); +}; + /** * Get a list of firewall rules. * @@ -499,7 +604,7 @@ Compute.prototype.listDisks = function(options, callback) { * projectId: 'grape-spaceship-123' * }); * - * compute.listFirewalls( + * compute.getFirewalls( * { * zone: 'europe-west1-b', * filter: { @@ -509,7 +614,7 @@ Compute.prototype.listDisks = function(options, callback) { * }], * }, callback); */ -Compute.prototype.listFirewalls = function(options, callback) { +Compute.prototype.getFirewalls = function(options, callback) { if (!callback) { callback = options; } diff --git a/lib/compute/instance.js b/lib/compute/instance.js index f221dd5d8b0..f26bc85d44b 100644 --- a/lib/compute/instance.js +++ b/lib/compute/instance.js @@ -56,10 +56,10 @@ function Instance(zone, name, metadata) { this.metadata = metadata; if (!util.is(this.name, 'string')) { - throw Error('An instance name is needed to create a Compute Instance.'); + throw Error('A name is needed to use a Compute Engine Instance.'); } if (!this.zone) { - throw Error('A zone is needed to create a Compute Instance.'); + throw Error('A zone is needed to use a Compute Engine Instance.'); } } @@ -76,6 +76,27 @@ Instance.prototype.delete = function(callback) { this.makeReq_('DELETE', '', null, true, callback); }; +/** + * Get the instances's metadata. + * + * @param {function=} callback - The callback function. + * + * @example + * instance.getMetadata(function(err, metadata, apiResponse) {}); + */ +Instance.prototype.getMetadata = function(callback) { + callback = callback || util.noop; + var self = this; + this.makeReq_('GET', '', null, null, function(err, resp) { + if (err) { + callback(err); + return; + } + self.metadata = resp; + callback(null, self.metadata, resp); + }); +}; + /** * Attach a disk to the instance. * diff --git a/lib/compute/region.js b/lib/compute/region.js index 83298399b48..fa688269dfc 100644 --- a/lib/compute/region.js +++ b/lib/compute/region.js @@ -46,7 +46,7 @@ function Region(compute, name) { this.compute = compute; if (!this.name) { - throw new Error('A name is needed to use Google Cloud Compute Region.'); + throw new Error('A name is needed to use a Compute Engine Region.'); } } diff --git a/lib/compute/snapshot.js b/lib/compute/snapshot.js new file mode 100644 index 00000000000..83297edc798 --- /dev/null +++ b/lib/compute/snapshot.js @@ -0,0 +1,112 @@ +/*! + * Copyright 2014 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*! + * @module compute/disk + */ + +'use strict'; + +/** + * @type {module:common/util} + * @private + */ +var util = require('../common/util.js'); + +/** + * Create a Snapshot object to interact with a Google Compute Engine snapshot. + * + * @constructor + * @alias module:compute/snapshot + * + * @throws {Error} if a snapshot name is not provided. + * + * @param {module:compute} compute - The Compute module this snapshot belongs + * to. + * @param {string} name - Snapshot name. + * @param {object=} metadata - Snapshot metadata. + * + * @example + * var gcloud = require('gcloud')({ + * keyFilename: '/path/to/keyfile.json' + * }); + * + * var compute = gcloud.compute(); + * + * var disk = compute.snapshot('snapshot-name'); + */ +function Snapshot(compute, name, metadata) { + this.compute = compute; + this.name = name; + this.metadata = metadata; + + if (!util.is(this.name, 'string')) { + throw new Error('A name is needed to use a Compute Engine Snapshot.'); + } +} + +/** + * Delete the snapshot. + * + * @param {function} callback - The callback function. + * + * @example + * snapshot.delete(function(err) {}); + */ +Snapshot.prototype.delete = function(callback) { + callback = callback || util.noop; + this.makeReq_('DELETE', '', null, true, callback); +}; + +/** + * Get the snapshots's metadata. + * + * @param {function=} callback - The callback function. + * + * @example + * snapshots.getMetadata(function(err, metadata, apiResponse) {}); + */ +Snapshot.prototype.getMetadata = function(callback) { + callback = callback || util.noop; + var self = this; + this.makeReq_('GET', '', null, null, function(err, resp) { + if (err) { + callback(err); + return; + } + self.metadata = resp; + callback(null, self.metadata, resp); + }); +}; + +/** + * Make a new request object from the provided arguments and wrap the callback + * to intercept non-successful responses. + * + * @private + * + * @param {string} method - Action. + * @param {string} path - Request path. + * @param {*} query - Request query object. + * @param {*} body - Request body contents. + * @param {function} callback - The callback function. + */ +Snapshot.prototype.makeReq_ = function(method, path, query, body, callback) { + path = '/global/snapshots/' + this.name + path; + this.compute.makeReq_(method, path, query, body, callback); +}; + +module.exports = Snapshot; \ No newline at end of file diff --git a/lib/compute/zone.js b/lib/compute/zone.js index f406efeb0f8..7712ac60156 100644 --- a/lib/compute/zone.js +++ b/lib/compute/zone.js @@ -64,7 +64,7 @@ function Zone(compute, name) { this.compute = compute; if (!this.name) { - throw new Error('A name is needed to use Google Cloud Compute Zone.'); + throw new Error('A name is needed to use a Compute Engine Zone.'); } } @@ -117,26 +117,27 @@ Zone.prototype.disk = function(name) { * * @param {string} name - Name of the instance. * @param {object} options - Options for instance creation. - * @param {string} machineType - Type of the instance. - * @param {object[]} disks - Disks to attach to the instance. - * @param {boolean} disks[].createNew - True if the disk has to be created. - * Default value is false. - * @param {string} disks[].source - Source for the disk to be created, either an - * image or a snapshot. - * @param {string=} disks[].diskType - Type of the disk. - * @param {number=} disks[].sizeGb - Size of the disk in GB. - * @param {boolean=} disks[].boot - True of the disk is a boot disk. - * @param {string=} disks[].mode - Attach mode, either `READ_ONLY` or + * @param {string} options.machineType - Type of the instance. + * @param {object[]} options.disks - Disks to attach to the instance. + * @param {boolean} options.disks[].createNew - True if the disk has to be + * created. Default value is false. + * @param {string} options.disks[].source - Source for the disk to be created, + * either an image or a snapshot. + * @param {string=} options.disks[].diskType - Type of the disk. + * @param {number=} options.disks[].sizeGb - Size of the disk in GB. + * @param {boolean=} options.disks[].boot - True of the disk is a boot disk. + * @param {string=} options.disks[].mode - Attach mode, either `READ_ONLY` or * `READ_WRITE`. Default value is `READ_WRITE`. - * @param {number=} disks[].index - Zero based index assigned from the instance - * to the disk. - * @param {string=} disks[].deviceName - Device name assigned from the instance - * to the disk. - * @param {boolean=} disks[].autoDelete - If true the disk is deleted when the - * instance is deleted. - * @param {object[]} networkInterfaces - Network interfaces for the instance. - * @param {string} networkInterfaces[].network - Full/partial URL of a network - * interface for this instance. + * @param {number=} options.disks[].index - Zero based index assigned from the + * instance to the disk. + * @param {string=} options.disks[].deviceName - Device name assigned from the + * instance to the disk. + * @param {boolean=} options.disks[].autoDelete - If true the disk is deleted + * when the instance is deleted. + * @param {object[]} options.networkInterfaces - Network interfaces for the + * instance. + * @param {string} options.networkInterfaces[].network - Full/partial URL of a + * network interface for this instance. * @param {function} callback - The callback function. * * @example @@ -252,18 +253,15 @@ Zone.prototype.createInstance = function(name, options, callback) { }); var self = this; - this.makeReq_( - 'POST', - '/instances', - query, - body, function(err, resp) { - if (err) { - callback(err); - return; - } - var instance = new Instance(self, name); - callback(null, instance, resp); - }); + this.makeReq_('POST', '/instances', query, body, function(err, resp) { + if (err) { + callback(err); + return; + } + var instance = self.instance(name); + instance.metadata = options; + callback(null, instance, resp); + }); }; /** @@ -295,7 +293,7 @@ Zone.prototype.createInstance = function(name, options, callback) { * * var myZone = compute.zone('zone-name'); * - * myZone.listInstances( + * myZone.getInstances( * { * filter: { * fieldName : 'name', @@ -304,7 +302,7 @@ Zone.prototype.createInstance = function(name, options, callback) { * }], * }, callback); */ -Zone.prototype.listInstances = function(options, callback) { +Zone.prototype.getInstances = function(options, callback) { if (util.is(options, 'function')) { callback = options; options = {}; @@ -336,29 +334,24 @@ Zone.prototype.listInstances = function(options, callback) { } var self = this; - this.makeReq_( - 'GET', - '/instances', - query, - null, function(err, resp) { - if (err) { - callback(err); - return; - } - if (resp.items) { - var instances = []; - for (var i = 0; i < resp.items.length; i++) { - var instance = new Instance( - self, - resp.items[i].name, - resp.items[i]); - instances.push(instance); - } - callback(null, instances); - } else { - callback(null, []); + this.makeReq_('GET', '/instances', query, null, function(err, resp) { + if (err) { + callback(err); + return; + } + if (resp.items) { + var instances = []; + for (var i = 0; i < resp.items.length; i++) { + var metadata = resp.items[i]; + var instance = self.instance(metadata.name); + instance.metadata = metadata; + instances.push(instance); } - }); + callback(null, instances); + } else { + callback(null, []); + } + }); }; /** @@ -369,9 +362,9 @@ Zone.prototype.listInstances = function(options, callback) { * * @param {string} name - Name of the disk. * @param {object} options - Options for disk creation. - * @param {string=} sourceImage - Source image for the disk. - * @param {string=} sourceSnapshot - Source snapshot for the disk. - * @param {number=} sizeGb - Size of the disk in GB. + * @param {string=} options.sourceImage - Source image for the disk. + * @param {string=} options.sourceSnapshot - Source snapshot for the disk. + * @param {number=} options.sizeGb - Size of the disk in GB. * @param {function} callback - The callback function. * * @example @@ -426,18 +419,15 @@ Zone.prototype.createDisk = function(name, options, callback) { body.name = name; var self = this; - this.makeReq_( - 'POST', - '/disks', - query, - body, function(err, resp) { - if (err) { - callback(err); - return; - } - var disk = new Disk(self, name, options.sizeGb); - callback(null, disk, resp); - }); + this.makeReq_('POST', '/disks', query, body, function(err, resp) { + if (err) { + callback(err); + return; + } + var disk = self.disk(name); + self.metadata = options; + callback(null, disk, resp); + }); }; /** @@ -468,7 +458,7 @@ Zone.prototype.createDisk = function(name, options, callback) { * * var myZone = compute.zone('zone-name'); * - * myZone.listDisks( + * myZone.getDisks( * { * filter: { * fieldName : 'name', @@ -477,7 +467,7 @@ Zone.prototype.createDisk = function(name, options, callback) { * }], * }, callback); */ -Zone.prototype.listDisks = function(options, callback) { +Zone.prototype.getDisks = function(options, callback) { if (util.is(options, 'function')) { callback = options; options = {}; @@ -509,30 +499,24 @@ Zone.prototype.listDisks = function(options, callback) { } var self = this; - this.makeReq_( - 'GET', - '/disks', - query, - null, function(err, resp) { - if (err) { - callback(err); - return; - } - if (resp.items) { - var disks = []; - for (var i = 0; i < resp.items.length; i++) { - var disk = new Disk( - self, - resp.items[i].name, - resp.items[i].sizeGb, - resp.items[i]); - disks.push(disk); - } - callback(null, disks); - } else { - callback(null, []); + this.makeReq_('GET', '/disks', query, null, function(err, resp) { + if (err) { + callback(err); + return; + } + if (resp.items) { + var disks = []; + for (var i = 0; i < resp.items.length; i++) { + var metadata = resp.items[i]; + var disk = self.disk(metadata.name); + disk.metadata = metadata; + disks.push(disk); } - }); + callback(null, disks); + } else { + callback(null, []); + } + }); }; /** From 91a901ea627ce502c120e710c482eec71577854c Mon Sep 17 00:00:00 2001 From: Marco Ziccardi Date: Thu, 23 Jul 2015 10:50:59 +0200 Subject: [PATCH 5/9] Add Address support to Compute module, minor refactoring of aggregated list functions, minor updates to documentation --- lib/compute/address.js | 117 +++++++++++++++++++++++++++ lib/compute/disk.js | 2 +- lib/compute/index.js | 126 ++++++++++++++++++++++++----- lib/compute/region.js | 171 +++++++++++++++++++++++++++++++++++++++- lib/compute/snapshot.js | 2 +- lib/compute/zone.js | 3 +- 6 files changed, 396 insertions(+), 25 deletions(-) create mode 100644 lib/compute/address.js diff --git a/lib/compute/address.js b/lib/compute/address.js new file mode 100644 index 00000000000..9f9cf84b334 --- /dev/null +++ b/lib/compute/address.js @@ -0,0 +1,117 @@ +/*! + * Copyright 2014 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*! + * @module compute/address + */ + +'use strict'; + +/** + * @type {module:common/util} + * @private + */ +var util = require('../common/util.js'); + +/** + * Create an Address object to interact with a Google Compute Engine address. + * + * @constructor + * @alias module:region/address + * + * @throws {Error} if an address name or a region are not provided. + * + * @param {module:region} region - The Google Compute Engine region this + * address belongs to. + * @param {string} name - The name of the address. + * @param {object=} metadata - Address metadata. + * + * @example + * var gcloud = require('gcloud')({ + * keyFilename: '/path/to/keyfile.json' + * }); + * + * var compute = gcloud.compute(); + * + * var myRegion = compute.region('region-name'); + * + * var address = myRegion.address('address1'); + */ +function Address(region, name, metadata) { + this.region = region; + this.name = name; + this.metadata = metadata; + + if (!util.is(this.name, 'string')) { + throw new Error('A name is needed to use a Compute Engine Address.'); + } + if (!this.region) { + throw new Error('A region is needed to use a Compute Engine Address.'); + } +} + +/** + * Delete the address. + * + * @param {function} callback - The callback function. + * + * @example + * address.delete(function(err) {}); + */ +Address.prototype.delete = function(callback) { + callback = callback || util.noop; + this.makeReq_('DELETE', '', null, true, callback); +}; + +/** + * Get the address' metadata. + * + * @param {function=} callback - The callback function. + * + * @example + * address.getMetadata(function(err, metadata, apiResponse) {}); + */ +Address.prototype.getMetadata = function(callback) { + callback = callback || util.noop; + var self = this; + this.makeReq_('GET', '', null, null, function(err, resp) { + if (err) { + callback(err); + return; + } + self.metadata = resp; + callback(null, self.metadata, resp); + }); +}; + +/** + * Make a new request object from the provided arguments and wrap the callback + * to intercept non-successful responses. + * + * @private + * + * @param {string} method - Action. + * @param {string} path - Request path. + * @param {*} query - Request query object. + * @param {*} body - Request body contents. + * @param {function} callback - The callback function. + */ +Address.prototype.makeReq_ = function(method, path, query, body, callback) { + path = '/addresses/' + this.name + path; + this.region.makeReq_(method, path, query, body, callback); +}; + +module.exports = Address; \ No newline at end of file diff --git a/lib/compute/disk.js b/lib/compute/disk.js index 983cad8ffc9..f16e788656c 100644 --- a/lib/compute/disk.js +++ b/lib/compute/disk.js @@ -36,7 +36,7 @@ var util = require('../common/util.js'); * * @param {module:zone} zone - The Google Compute Engine zone this * disk belongs to. - * @param {string} name - Disk name. + * @param {string} name - Name of the disk. * @param {object=} metadata - Disk metadata. * * @example diff --git a/lib/compute/index.js b/lib/compute/index.js index 944d1a499a5..7ca4ab1df91 100644 --- a/lib/compute/index.js +++ b/lib/compute/index.js @@ -32,18 +32,6 @@ var Region = require('./region.js'); */ var Zone = require('./zone.js'); -/** - * @type {module:compute/instance} - * @private - */ -var Instance = require('./instance.js'); - -/** - * @type {module:compute/disk} - * @private - */ -var Disk = require('./disk.js'); - /** * @type {module:compute/firewall} * @private @@ -300,8 +288,7 @@ Compute.prototype.createFirewall = function(name, options, callback) { * @throws {Error} if a malformed filter is provided. * * @param {object} options - Instance search options. - * @param {number=} options.maxResults - Maximum number of instances to - * return. + * @param {number=} options.maxResults - Maximum number of instances to return. * @param {object=} options.filter - Search filter. * @param {string} options.filter.fieldName - Instance field to consider in this * filter. @@ -378,10 +365,9 @@ Compute.prototype.getInstances = function(options, callback) { var zoneInstances = resp.items[zoneName].instances; if (zoneInstances) { for (var i = 0; i < zoneInstances.length; i++) { - var instance = new Instance( - zone, - zoneInstances[i].name, - zoneInstances[i]); + var metadata = zoneInstances[i]; + var instance = zone.instance(metadata.name); + instance.metadata = metadata; instances.push(instance); } } @@ -476,12 +462,111 @@ Compute.prototype.getDisks = function(options, callback) { var zoneDisks = resp.items[zoneName].disks; if (zoneDisks) { for (var i = 0; i < zoneDisks.length; i++) { - var disk = new Disk(zone, zoneDisks[i].name, zoneDisks[i]); + var metadata = zoneDisks[i]; + var disk = zone.disk(metadata.name); + disk.metadata = metadata; disks.push(disk); } } } - callback(null, disks); + } else { + callback(null, []); + } + }); +}; + +/** + * Get a list of addresses. + * + * @throws {Error} if a malformed filter is provided. + * + * @param {object} options - Address search options. + * @param {number=} options.maxResults - Maximum number of addresses to return. + * @param {object=} options.filter - Search filter. + * @param {string} options.filter.fieldName - Address field to consider in this + * filter. + * @param {string} options.filter.operatorString - Filter operator, can be + * either 'eq' or 'ne'. + * @param {string} options.filter.literalString - String to compare the address + * field to. Can be a regular expression. + * @param {string=} pageToken - Page identifier used in paginated search. + * @param {function} callback - The callback function. + * + * @example + * var callback = function(err, addresses) { + * // `addresses` is an array of `Address` objects. + * }; + * + * var compute = gcloud.compute({ + * projectId: 'grape-spaceship-123' + * }); + * + * compute.getAddresses( + * { + * filter: { + * fieldName : 'name', + * operatorString : 'eq', + * literalString : 'address-[0-9]' + * }], + * }, callback); + */ +Compute.prototype.getAddresses = function(options, callback) { + if (!callback) { + callback = options; + } + + var query = {}; + + if (options) { + if (util.is(options.maxResults, 'number')) { + query.maxResults = options.maxResults; + } + if (options.filter) { + if (!util.is(options.filter.fieldName, 'string') || + !util.is(options.filter.operatorString, 'string') || + !util.is(options.filter.literalString, 'string')) { + throw new Error( + 'A filter must have name, comparisonString and literal fields.'); + } + if (options.filter.operatorString !== 'eq' && + options.filter.operatorString !== 'ne') { + throw new Error('Filter operator must be either eq or ne'); + } + query.filter = + options.filter.fieldName + ' ' + + options.filter.operatorString + ' \'' + + options.filter.literalString + '\''; + } + if (util.is(options.pageToken, 'string')) { + query.pageToken = options.pageToken; + } + } + + var self = this; + this.makeReq_( + 'GET', + '/aggregated/addresses', + query, + null, function(err, resp) { + if (err) { + callback(err); + return; + } + if (resp.items) { + var addresses = []; + for (var regionName in resp.items) { + var region = self.region(regionName); + var regionAddresses = resp.items[regionName].addresses; + if (regionAddresses) { + for (var i = 0; i < regionAddresses.length; i++) { + var metadata = regionAddresses[i]; + var address = region.address(metadata.name); + address.metadata = metadata; + addresses.push(address); + } + } + } + callback(null, addresses); } else { callback(null, []); } @@ -578,6 +663,7 @@ Compute.prototype.getSnapshots = function(options, callback) { }); }; + /** * Get a list of firewall rules. * diff --git a/lib/compute/region.js b/lib/compute/region.js index fa688269dfc..e5517601e23 100644 --- a/lib/compute/region.js +++ b/lib/compute/region.js @@ -20,6 +20,18 @@ 'use strict'; +/** + * @type {module:compute/address} + * @private + */ +var Address = require('./address.js'); + +/** + * @type {module:common/util} + * @private + */ +var util = require('../common/util.js'); + /** * Create a Region object to interact with a Google Compute Engine region. * @@ -50,6 +62,163 @@ function Region(compute, name) { } } +/** + * Get a reference to a Google Compute Engine address in this region. + * + * @param {string} name - Name of the existing address. + * @return {module:compute/address} + * + * @example + * var gcloud = require('gcloud')({ + * keyFilename: '/path/to/keyfile.json' + * }); + * + * var compute = gcloud.compute(); + * + * var myRegion = compute.region('region-name'); + * + * var address = myRegion.address('address-name'); + */ +Region.prototype.address = function(name) { + return new Address(this, name); +}; + +/** + * Create an address in this region. + * + * @throws {Error} if an address name or a callback are not provided. + * + * @param {string} name - Name of the address. + * @param {function} callback - The callback function. + * + * @example + * var callback = function(err, address, apiResponse) { + * // `address` is a Address object. + * }; + * + * var compute = gcloud.compute({ + * projectId: 'grape-spaceship-123' + * }); + * + * var myRegion = compute.region('region-name'); + * + * myRegion.createDisk('new-address', callback); + */ +Region.prototype.createAddress = function(name, callback) { + if (!name) { + throw new Error('A name is required to create an address.'); + } else if (!/^(?:[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?)$/.test(name)) { + throw new Error('Name must match [a-z]([-a-z0-9]{0,61}[a-z0-9])?'); + } + if (!callback) { + throw new Error('A callback must be defined.'); + } + + var body = { + name: name + }; + + var self = this; + this.makeReq_('POST', '/addresses', null, body, function(err, resp) { + if (err) { + callback(err); + return; + } + var address = self.address(name); + callback(null, address, resp); + }); +}; + +/** + * Get a list of addresses in this region. + * + * @throws {Error} if a malformed filter is provided. + * + * @param {object} options - Address search options. + * @param {number=} options.maxResults - Maximum number of addresses to return. + * @param {object=} options.filter - Search filter. + * @param {string} options.filter.fieldName - Address field to consider in this + * filter. + * @param {string} options.filter.operatorString - Filter operator, can be + * either 'eq' or 'ne'. + * @param {string} options.filter.literalString - String to compare the address + * field to. Can be a regular expression. + * @param {string=} pageToken - Page identifier used in paginated search. + * @param {function} callback - The callback function. + * + * @example + * var callback = function(err, addresses) { + * // `addresses` is an array of `Address` objects. + * }; + * + * var compute = gcloud.compute({ + * projectId: 'grape-spaceship-123' + * }); + * + * var myRegion = compute.region('region-name'); + * + * myRegion.getAddresses( + * { + * filter: { + * fieldName : 'name', + * operatorString : 'eq', + * literalString : 'address-[0-9]' + * }], + * }, callback); + */ +Region.prototype.getAddresses = function(options, callback) { + if (!callback) { + callback = options; + } + + var query = {}; + + if (options) { + if (util.is(options.maxResults, 'number')) { + query.maxResults = options.maxResults; + } + if (options.filter) { + if (!util.is(options.filter.fieldName, 'string') || + !util.is(options.filter.operatorString, 'string') || + !util.is(options.filter.literalString, 'string')) { + throw new Error( + 'A filter must have name, comparisonString and literal fields.'); + } + if (options.filter.operatorString !== 'eq' && + options.filter.operatorString !== 'ne') { + throw new Error('Filter operator must be either eq or ne'); + } + query.filter = + options.filter.fieldName + ' ' + + options.filter.operatorString + ' \'' + + options.filter.literalString + '\''; + } + if (util.is(options.pageToken, 'string')) { + query.pageToken = options.pageToken; + } + } + + var self = this; + this.makeReq_('GET', '/addresses', query, null, function(err, resp) { + if (err) { + callback(err); + return; + } + if (resp.items) { + var addresses = []; + for (var i = 0; i < resp.items.length; i++) { + var metadata = resp.items[i]; + var address = self.address(metadata.name); + address.metadata = metadata; + addresses.push(address); + } + callback(null, addresses); + } else { + callback(null, []); + } + }); +}; + /** * Make a new request object from the provided arguments and wrap the callback * to intercept non-successful responses. @@ -67,4 +236,4 @@ Region.prototype.makeReq_ = function(method, path, query, body, callback) { this.compute.makeReq_(method, path, query, body, callback); }; -module.exports = Region; \ No newline at end of file +module.exports = Region; diff --git a/lib/compute/snapshot.js b/lib/compute/snapshot.js index 83297edc798..57a0848f1fc 100644 --- a/lib/compute/snapshot.js +++ b/lib/compute/snapshot.js @@ -77,7 +77,7 @@ Snapshot.prototype.delete = function(callback) { * @param {function=} callback - The callback function. * * @example - * snapshots.getMetadata(function(err, metadata, apiResponse) {}); + * snapshot.getMetadata(function(err, metadata, apiResponse) {}); */ Snapshot.prototype.getMetadata = function(callback) { callback = callback || util.noop; diff --git a/lib/compute/zone.js b/lib/compute/zone.js index 7712ac60156..93fac0fa7db 100644 --- a/lib/compute/zone.js +++ b/lib/compute/zone.js @@ -270,8 +270,7 @@ Zone.prototype.createInstance = function(name, options, callback) { * @throws {Error} if a malformed filter is provided. * * @param {object} options - Instance search options. - * @param {number=} options.maxResults - Maximum number of instances to - * return. + * @param {number=} options.maxResults - Maximum number of instances to return. * @param {object=} options.filter - Search filter. * @param {string} options.filter.fieldName - Instance field to consider in this * filter. From c99bbebdb14b1ae3dfe42e4d5427cab39b306f28 Mon Sep 17 00:00:00 2001 From: Marco Ziccardi Date: Sun, 26 Jul 2015 20:57:26 +0200 Subject: [PATCH 6/9] Compute: add pagination support to all list methods, make filters simple strings, simplify list methods --- lib/compute/index.js | 682 ++++++++++++++++++++++-------------------- lib/compute/region.js | 138 +++++---- lib/compute/zone.js | 247 ++++++++------- 3 files changed, 573 insertions(+), 494 deletions(-) diff --git a/lib/compute/index.js b/lib/compute/index.js index 7ca4ab1df91..0ca7321253f 100644 --- a/lib/compute/index.js +++ b/lib/compute/index.js @@ -20,6 +20,8 @@ 'use strict'; +var extend = require('extend'); + /** * @type {module:compute/region} * @private @@ -50,6 +52,12 @@ var Snapshot = require('./snapshot.js'); */ var util = require('../common/util.js'); +/** + * @type {module:common/streamrouter} + * @private + */ +var streamRouter = require('../common/stream-router.js'); + /** * Required scopes for Google Compute Engine API. * @const {array} @@ -191,8 +199,8 @@ Compute.prototype.snapshot = function(name) { * @throws {Error} if a firewall name or firewall options are not provided. * If allowed ports, source tags or source ranges are not provided. * - * @param {module:compute} compute - The Google Compute Engine instance this - * this firewall belongs to. + * @param {module:compute} compute - The Google Compute Engine object this + * firewall belongs to. * @param {string} name - Name of the firewall. * @param {object} options - Firewall options. * @param {string=} options.description - Description of the firewall. @@ -285,17 +293,15 @@ Compute.prototype.createFirewall = function(name, options, callback) { /** * Get a list of instances. * - * @throws {Error} if a malformed filter is provided. - * * @param {object} options - Instance search options. + * @param {boolean=} options.autoPaginate - Have pagination handled + * automatically. Default: true. * @param {number=} options.maxResults - Maximum number of instances to return. - * @param {object=} options.filter - Search filter. - * @param {string} options.filter.fieldName - Instance field to consider in this - * filter. - * @param {string} options.filter.operatorString - Filter operator, can be - * either 'eq' or 'ne'. - * @param {string} options.filter.literalString - String to compare the instance - * field to. Can be a regular expression. + * @param {string=} options.filter - Search filter. The filter must contain: + * `FIELD_NAME COMPARISON_STRING LITERAL_STRING`. `FIELD_NAME` is the name + * of the field to compare. `COMPARISON` is the comparison operator that can + * be either `eq` or `ne`. `LITERAL_STRING` is the string to filter to. + * For string fields `LITERAL_STRING` can be a regular expression. * @param {string=} pageToken - Page identifier used in paginated search. * @param {function} callback - The callback function. * @@ -304,18 +310,46 @@ Compute.prototype.createFirewall = function(name, options, callback) { * // `instances` is an array of `Instance` objects. * }; * - * var compute = gcloud.compute({ - * projectId: 'grape-spaceship-123' - * }); - * * compute.getInstances( * { - * filter: { - * fieldName : 'name', - * operatorString : 'eq', - * literalString : 'instance-[0-9]' - * }], + * filter: 'name eq instance-[0-9]' * }, callback); + * + * //- + * // To control how many API requests are made and page through the results + * // manually, set `autoPaginate` to `false`. + * //- + * var callback = function(err, instances, nextQuery, apiResponse) { + * if (nextQuery) { + * // More results exist. + * compute.getInstances(nextQuery, callback); + * } + * }; + * + * compute.getInstances({ + * autoPaginate: false + * }, callback); + * + * //- + * // Get the instances from your project as a readable object stream. + * //- + * compute.getInstances() + * .on('error', console.error) + * .on('data', function(instance) { + * // `instance` is an `Instance` object. + * }) + * .on('end', function() { + * // All instances retrieved. + * }); + * + * //- + * // If you anticipate many results, you can end a stream early to prevent + * // unnecessary processing and API requests. + * //- + * compute.getInstances() + * .on('data', function(instance) { + * this.end(); + * }); */ Compute.prototype.getInstances = function(options, callback) { if (util.is(options, 'function')) { @@ -323,76 +357,54 @@ Compute.prototype.getInstances = function(options, callback) { options = {}; } - var query = {}; - - if (util.is(options.maxResults, 'number')) { - query.maxResults = options.maxResults; - } - if (options.filter) { - if (!util.is(options.filter.fieldName, 'string') || - !util.is(options.filter.operatorString, 'string') || - !util.is(options.filter.literalString, 'string')) { - throw new Error( - 'A filter must have name, comparisonString and literal fields.'); - } - if (options.filter.operatorString !== 'eq' && - options.filter.operatorString !== 'ne') { - throw new Error('Filter operator must be either eq or ne'); - } - query.filter = - options.filter.fieldName + ' ' + - options.filter.operatorString + ' \'' + - options.filter.literalString + '\''; - } - if (util.is(options.pageToken, 'string')) { - query.pageToken = options.pageToken; - } + options = options || {}; var self = this; this.makeReq_( 'GET', '/aggregated/instances', - query, + options, null, function(err, resp) { if (err) { callback(err); return; } - if (resp.items) { - var instances = []; - for (var zoneName in resp.items) { - var zone = self.zone(zoneName.replace('zones/', '')); - var zoneInstances = resp.items[zoneName].instances; - if (zoneInstances) { - for (var i = 0; i < zoneInstances.length; i++) { - var metadata = zoneInstances[i]; - var instance = zone.instance(metadata.name); - instance.metadata = metadata; - instances.push(instance); - } - } - } - callback(null, instances); - } else { - callback(null, []); + + var nextQuery = null; + + if (resp.nextPageToken) { + nextQuery = extend({}, options, { + pageToken: resp.nextPageToken + }); + } + + var items = resp.items || {}; + var instances = []; + var appendInstance = function(instanceObject) { + var instance = zone.instance(instanceObject.name); + instance.metadata = instanceObject; + instances.push(instance); + }; + for (var zoneName in items) { + var zone = self.zone(zoneName.replace('zones/', '')); + (items[zoneName].instances || []).forEach(appendInstance); } + callback(null, instances, nextQuery, resp); }); }; /** * Get a list of disks. * - * @throws {Error} if a malformed filter is provided. - * * @param {object} options - Disk search options. + * @param {boolean=} options.autoPaginate - Have pagination handled + * automatically. Default: true. * @param {number=} options.maxResults - Maximum number of disks to return. - * @param {object=} options.filter - Search filter. - * @param {string} options.filter.fieldName - Disk field to consider in this - * filter. - * @param {string} options.filter.operatorString - Filter operator, can be - * either 'eq' or 'ne'. - * @param {string} options.filter.literalString - String to compare the disk - * field to. Can be a regular expression. + * @param {string=} options.filter - Search filter. The filter must contain: + * `FIELD_NAME COMPARISON_STRING LITERAL_STRING`. `FIELD_NAME` is the name + * of the field to compare. `COMPARISON` is the comparison operator that can + * be either `eq` or `ne`. `LITERAL_STRING` is the string to filter to. + * For string fields `LITERAL_STRING` can be a regular expression. * @param {string=} pageToken - Page identifier used in paginated search. * @param {function} callback - The callback function. * @@ -401,18 +413,46 @@ Compute.prototype.getInstances = function(options, callback) { * // `disks` is an array of `Disk` objects. * }; * - * var compute = gcloud.compute({ - * projectId: 'grape-spaceship-123' - * }); - * * compute.getDisks( * { - * filter: { - * fieldName : 'name', - * operatorString : 'eq', - * literalString : 'disk-[0-9]' - * }], + * filter: 'name eq disk-[0-9]' * }, callback); + * + * //- + * // To control how many API requests are made and page through the results + * // manually, set `autoPaginate` to `false`. + * //- + * var callback = function(err, disks, nextQuery, apiResponse) { + * if (nextQuery) { + * // More results exist. + * compute.getDisks(nextQuery, callback); + * } + * }; + * + * compute.getDisks({ + * autoPaginate: false + * }, callback); + * + * //- + * // Get the disks from your project as a readable object stream. + * //- + * compute.getDisks() + * .on('error', console.error) + * .on('data', function(disk) { + * // `disk` is an `Disk` object. + * }) + * .on('end', function() { + * // All disks retrieved. + * }); + * + * //- + * // If you anticipate many results, you can end a stream early to prevent + * // unnecessary processing and API requests. + * //- + * compute.getDisks() + * .on('data', function(disk) { + * this.end(); + * }); */ Compute.prototype.getDisks = function(options, callback) { if (util.is(options, 'function')) { @@ -420,75 +460,50 @@ Compute.prototype.getDisks = function(options, callback) { options = {}; } - var query = {}; + options = options || {}; - if (util.is(options.maxResults, 'number')) { - query.maxResults = options.maxResults; - } - if (options.filter) { - if (!util.is(options.filter.fieldName, 'string') || - !util.is(options.filter.operatorString, 'string') || - !util.is(options.filter.literalString, 'string')) { - throw new Error( - 'A filter must have name, comparisonString and literal fields.'); + var self = this; + this.makeReq_('GET', '/aggregated/disks', options, null, function(err, resp) { + if (err) { + callback(err); + return; } - if (options.filter.operatorString !== 'eq' && - options.filter.operatorString !== 'ne') { - throw new Error('Filter operator must be either eq or ne'); + + var nextQuery = null; + + if (resp.nextPageToken) { + nextQuery = extend({}, options, { + pageToken: resp.nextPageToken + }); } - query.filter = - options.filter.fieldName + ' ' + - options.filter.operatorString + ' \'' + - options.filter.literalString + '\''; - } - if (util.is(options.pageToken, 'string')) { - query.pageToken = options.pageToken; - } - var self = this; - this.makeReq_( - 'GET', - '/aggregated/disks', - query, - null, function(err, resp) { - if (err) { - callback(err); - return; - } - if (resp.items) { - var disks = []; - for (var zoneName in resp.items) { - var zone = self.zone(zoneName.replace('zones/', '')); - var zoneDisks = resp.items[zoneName].disks; - if (zoneDisks) { - for (var i = 0; i < zoneDisks.length; i++) { - var metadata = zoneDisks[i]; - var disk = zone.disk(metadata.name); - disk.metadata = metadata; - disks.push(disk); - } - } - } - } else { - callback(null, []); - } - }); + var items = resp.items || {}; + var disks = []; + var appendDisk = function(diskObject) { + var disk = zone.disk(diskObject.name); + disk.metadata = diskObject; + disks.push(disk); + }; + for (var zoneName in items) { + var zone = self.zone(zoneName.replace('zones/', '')); + (items[zoneName].disks || []).forEach(appendDisk); + } + callback(null, disks, nextQuery, resp); + }); }; /** * Get a list of addresses. * - * @throws {Error} if a malformed filter is provided. - * * @param {object} options - Address search options. + * @param {boolean=} options.autoPaginate - Have pagination handled + * automatically. Default: true. * @param {number=} options.maxResults - Maximum number of addresses to return. - * @param {object=} options.filter - Search filter. - * @param {string} options.filter.fieldName - Address field to consider in this - * filter. - * @param {string} options.filter.operatorString - Filter operator, can be - * either 'eq' or 'ne'. - * @param {string} options.filter.literalString - String to compare the address - * field to. Can be a regular expression. + * @param {string=} options.filter - Search filter. The filter must contain: + * `FIELD_NAME COMPARISON_STRING LITERAL_STRING`. `FIELD_NAME` is the name + * of the field to compare. `COMPARISON` is the comparison operator that can + * be either `eq` or `ne`. `LITERAL_STRING` is the string to filter to. + * For string fields `LITERAL_STRING` can be a regular expression. * @param {string=} pageToken - Page identifier used in paginated search. * @param {function} callback - The callback function. * @@ -497,116 +512,149 @@ Compute.prototype.getDisks = function(options, callback) { * // `addresses` is an array of `Address` objects. * }; * - * var compute = gcloud.compute({ - * projectId: 'grape-spaceship-123' - * }); - * * compute.getAddresses( * { - * filter: { - * fieldName : 'name', - * operatorString : 'eq', - * literalString : 'address-[0-9]' - * }], + * filter: 'name eq address-[0-9]' * }, callback); + * + * //- + * // To control how many API requests are made and page through the results + * // manually, set `autoPaginate` to `false`. + * //- + * var callback = function(err, addresses, nextQuery, apiResponse) { + * if (nextQuery) { + * // More results exist. + * compute.getAddresses(nextQuery, callback); + * } + * }; + * + * compute.getAddresses({ + * autoPaginate: false + * }, callback); + * + * //- + * // Get the addresses from your project as a readable object stream. + * //- + * compute.getAddresses() + * .on('error', console.error) + * .on('data', function(address) { + * // `address` is an `Address` object. + * }) + * .on('end', function() { + * // All addresses retrieved. + * }); + * + * //- + * // If you anticipate many results, you can end a stream early to prevent + * // unnecessary processing and API requests. + * //- + * compute.getAddresses() + * .on('data', function(address) { + * this.end(); + * }); */ Compute.prototype.getAddresses = function(options, callback) { - if (!callback) { + if (util.is(options, 'function')) { callback = options; + options = {}; } - var query = {}; - - if (options) { - if (util.is(options.maxResults, 'number')) { - query.maxResults = options.maxResults; - } - if (options.filter) { - if (!util.is(options.filter.fieldName, 'string') || - !util.is(options.filter.operatorString, 'string') || - !util.is(options.filter.literalString, 'string')) { - throw new Error( - 'A filter must have name, comparisonString and literal fields.'); - } - if (options.filter.operatorString !== 'eq' && - options.filter.operatorString !== 'ne') { - throw new Error('Filter operator must be either eq or ne'); - } - query.filter = - options.filter.fieldName + ' ' + - options.filter.operatorString + ' \'' + - options.filter.literalString + '\''; - } - if (util.is(options.pageToken, 'string')) { - query.pageToken = options.pageToken; - } - } + options = options || {}; var self = this; this.makeReq_( 'GET', '/aggregated/addresses', - query, + options, null, function(err, resp) { if (err) { callback(err); return; } - if (resp.items) { - var addresses = []; - for (var regionName in resp.items) { - var region = self.region(regionName); - var regionAddresses = resp.items[regionName].addresses; - if (regionAddresses) { - for (var i = 0; i < regionAddresses.length; i++) { - var metadata = regionAddresses[i]; - var address = region.address(metadata.name); - address.metadata = metadata; - addresses.push(address); - } - } - } - callback(null, addresses); - } else { - callback(null, []); + + var nextQuery = null; + + if (resp.nextPageToken) { + nextQuery = extend({}, options, { + pageToken: resp.nextPageToken + }); + } + + var items = resp.items || {}; + var addresses = []; + var appendAddress = function(addressObject) { + var address = region.address(addressObject.name); + address.metadata = addressObject; + addresses.push(address); + }; + for (var regionName in items) { + var region = self.region(regionName.replace('regions/', '')); + (items[regionName].addresses || []).forEach(appendAddress); } + callback(null, addresses, nextQuery, resp); }); }; /** * Get a list of snapshots. * - * @throws {Error} if a malformed filter is provided. - * - * @param {object} options - Disk search options. + * @param {object} options - Snapshot search options. + * @param {boolean=} options.autoPaginate - Have pagination handled + * automatically. Default: true. * @param {number=} options.maxResults - Maximum number of snapshots to return. - * @param {object=} options.filter - Search filter. - * @param {string} options.filter.fieldName - Snapshot field to consider in this - * filter. - * @param {string} options.filter.operatorString - Filter operator, can be - * either 'eq' or 'ne'. - * @param {string} options.filter.literalString - String to compare the snapshot - * field to. Can be a regular expression. + * @param {string=} options.filter - Search filter. The filter must contain: + * `FIELD_NAME COMPARISON_STRING LITERAL_STRING`. `FIELD_NAME` is the name + * of the field to compare. `COMPARISON` is the comparison operator that can + * be either `eq` or `ne`. `LITERAL_STRING` is the string to filter to. + * For string fields `LITERAL_STRING` can be a regular expression. * @param {string=} pageToken - Page identifier used in paginated search. * @param {function} callback - The callback function. * * @example * var callback = function(err, snapshots) { - * // `disks` is an array of `Snapshot` objects. + * // `snapshots` is an array of `Snapshot` objects. * }; * - * var compute = gcloud.compute({ - * projectId: 'grape-spaceship-123' - * }); - * * compute.getSnapshots( * { - * filter: { - * fieldName : 'name', - * operatorString : 'eq', - * literalString : 'snapshot-[0-9]' - * }], + * filter: 'name eq snapshot-[0-9]' * }, callback); + * + * //- + * // To control how many API requests are made and page through the results + * // manually, set `autoPaginate` to `false`. + * //- + * var callback = function(err, snapshots, nextQuery, apiResponse) { + * if (nextQuery) { + * // More results exist. + * compute.getSnapshots(nextQuery, callback); + * } + * }; + * + * compute.getSnapshots({ + * autoPaginate: false + * }, callback); + * + * //- + * // Get the snapshots from your project as a readable object stream. + * //- + * compute.getSnapshots() + * .on('error', console.error) + * .on('data', function(snapshot) { + * // `snapshot` is an `Snapshot` object. + * }) + * .on('end', function() { + * // All snapshots retrieved. + * }); + * + * //- + * // If you anticipate many results, you can end a stream early to prevent + * // unnecessary processing and API requests. + * //- + * compute.getSnapshots() + * .on('data', function(snapshot) { + * this.end(); + * }); */ Compute.prototype.getSnapshots = function(options, callback) { if (util.is(options, 'function')) { @@ -614,70 +662,44 @@ Compute.prototype.getSnapshots = function(options, callback) { options = {}; } - var query = {}; + options = options || {}; - if (util.is(options.maxResults, 'number')) { - query.maxResults = options.maxResults; - } - if (options.filter) { - if (!util.is(options.filter.fieldName, 'string') || - !util.is(options.filter.operatorString, 'string') || - !util.is(options.filter.literalString, 'string')) { - throw new Error( - 'A filter must have name, comparisonString and literal fields.'); + var self = this; + this.makeReq_('GET', '/global/snapshots', options, null, function(err, resp) { + if (err) { + callback(err); + return; } - if (options.filter.operatorString !== 'eq' && - options.filter.operatorString !== 'ne') { - throw new Error('Filter operator must be either eq or ne'); + + var nextQuery = null; + + if (resp.nextPageToken) { + nextQuery = extend({}, options, { + pageToken: resp.nextPageToken + }); } - query.filter = - options.filter.fieldName + ' ' + - options.filter.operatorString + ' \'' + - options.filter.literalString + '\''; - } - if (util.is(options.pageToken, 'string')) { - query.pageToken = options.pageToken; - } - var self = this; - this.makeReq_('GET', - '/global/snapshots', - query, - null, function(err, resp) { - if (err) { - callback(err); - return; - } - if (resp.items) { - var snapshots = []; - for (var i = 0; i < resp.items.length; i++) { - var metadata = resp.items[i]; - var snapshot = self.snapshot(metadata.name); - snapshot.metadata = metadata; - snapshots.push(snapshot); - } - callback(null, snapshots); - } else { - callback(null, []); - } + var snapshots = (resp.items || []).map(function(snapshotObject) { + var snapshot = self.snapshot(snapshotObject.name); + snapshot.metadata = snapshotObject; + return snapshot; }); + callback(null, snapshots, nextQuery, resp); + }); }; - /** * Get a list of firewall rules. * - * @throws {Error} if a malformed filter is provided. - * * @param {object} options - Firewall search options. + * @param {boolean=} options.autoPaginate - Have pagination handled + * automatically. Default: true. * @param {number=} options.maxResults - Maximum number of firewalls to return. - * @param {object=} options.filter - Search filter. - * @param {string} options.filter.fieldName - Firewall field to consider in this - * filter. - * @param {string} options.filter.operatorString - Filter operator, can be - * either 'eq' or 'ne'. - * @param {string} options.filter.literalString - String to compare the firewall - * field to. Can be a regular expression. + * @param {string=} options.filter - Search filter. The filter must contain: + * `FIELD_NAME COMPARISON_STRING LITERAL_STRING`. `FIELD_NAME` is the name + * of the field to compare. `COMPARISON` is the comparison operator that can + * be either `eq` or `ne`. `LITERAL_STRING` is the string to filter to. + * For string fields `LITERAL_STRING` can be a regular expression. * @param {string=} pageToken - Page identifier used in paginated search. * @param {function} callback - The callback function. * @@ -686,81 +708,78 @@ Compute.prototype.getSnapshots = function(options, callback) { * // `firewalls` is an array of `Firewall` objects. * }; * - * var compute = gcloud.compute({ - * projectId: 'grape-spaceship-123' - * }); - * * compute.getFirewalls( * { * zone: 'europe-west1-b', - * filter: { - * fieldName : 'name', - * operatorString : 'eq', - * literalString : 'firewall-[0-9]' - * }], + * filter: 'name eq firewall-[0-9]' * }, callback); + * + * //- + * // To control how many API requests are made and page through the results + * // manually, set `autoPaginate` to `false`. + * //- + * var callback = function(err, firewalls, nextQuery, apiResponse) { + * if (nextQuery) { + * // More results exist. + * compute.getFirewalls(nextQuery, callback); + * } + * }; + * + * compute.getFirewalls({ + * autoPaginate: false + * }, callback); + * + * //- + * // Get the snapshots from your project as a readable object stream. + * //- + * compute.getSnapshots() + * .on('error', console.error) + * .on('data', function(firewall) { + * // `firewall` is an `Firewall` object. + * }) + * .on('end', function() { + * // All firewalls retrieved. + * }); + * + * //- + * // If you anticipate many results, you can end a stream early to prevent + * // unnecessary processing and API requests. + * //- + * compute.getFirewalls() + * .on('data', function(firewall) { + * this.end(); + * }); */ Compute.prototype.getFirewalls = function(options, callback) { - if (!callback) { + if (util.is(options, 'function')) { callback = options; + options = {}; } - var query = {}; + options = options || {}; - if (util.is(options.maxResults, 'number')) { - query.maxResults = options.maxResults; - } - if (options.filter) { - if (!util.is(options.filter.fieldName, 'string') || - !util.is(options.filter.operatorString, 'string') || - !util.is(options.filter.literalString, 'string')) { - throw new Error( - 'A filter must have name, comparisonString and literal fields.'); + var self = this; + this.makeReq_('GET', '/global/firewalls', options, null, function(err, resp) { + if (err) { + callback(err); + return; } - if (options.filter.operatorString !== 'eq' && - options.filter.operatorString !== 'ne') { - throw new Error('Filter operator must be either eq or ne'); + + var nextQuery = null; + + if (resp.nextPageToken) { + nextQuery = extend({}, options, { + pageToken: resp.nextPageToken + }); } - query.filter = - options.filter.fieldName + ' ' + - options.filter.operatorString + ' \'' + - options.filter.literalString + '\''; - } - if (util.is(options.pageToken, 'string')) { - query.pageToken = options.pageToken; - } - var self = this; - this.makeReq_( - 'GET', - '/global/firewalls', - query, - null, function(err, resp) { - if (err) { - callback(err); - return; - } - if (resp.items) { - var firewalls = []; - for (var i = 0; i < resp.items.length; i++) { - var firewall = new Firewall( - self, - resp.items[i].name, - { - description: resp.items[i].description, - network: resp.items[i].network, - allowed: resp.items[i].allowed, - sourceRanges: resp.items[i].sourceRanges, - sourceTags: resp.items[i].sourceTags, - targetTags: resp.items[i].targetTags - }); - firewalls.push(firewall); - } - callback(null, firewalls); - } else { - callback(null, []); - } + var firewalls = (resp.items || []).map(function(firewallObject) { + var firewall = self.firewall(firewallObject.name); + firewall.metadata = firewallObject; + return firewall; }); + callback(null, firewalls, nextQuery, resp); + }); }; /** @@ -789,5 +808,18 @@ Compute.prototype.makeReq_ = function(method, path, query, body, callback) { this.makeAuthorizedRequest_(reqOpts, callback); }; +/*! Developer Documentation + * + * These methods can be used with either a callback or as a readable object + * stream. `streamRouter` is used to add this dual behavior. + */ +streamRouter.extend(Compute, [ + 'getInstances', + 'getDisks', + 'getAddresses', + 'getSnapshots', + 'getFirewalls' +]); + module.exports = Compute; diff --git a/lib/compute/region.js b/lib/compute/region.js index e5517601e23..592f8a78bd9 100644 --- a/lib/compute/region.js +++ b/lib/compute/region.js @@ -20,6 +20,8 @@ 'use strict'; +var extend = require('extend'); + /** * @type {module:compute/address} * @private @@ -32,6 +34,12 @@ var Address = require('./address.js'); */ var util = require('../common/util.js'); +/** + * @type {module:common/streamrouter} + * @private + */ +var streamRouter = require('../common/stream-router.js'); + /** * Create a Region object to interact with a Google Compute Engine region. * @@ -40,8 +48,8 @@ var util = require('../common/util.js'); * * @throws {Error} if a region name is not provided. * - * @param {module:compute} compute - The Google Compute Engine instance this - * disk belongs to. + * @param {module:compute} compute - The Google Compute Engine object this + * region belongs to. * @param {string} name - Name of the region. * * @example @@ -132,17 +140,15 @@ Region.prototype.createAddress = function(name, callback) { /** * Get a list of addresses in this region. * - * @throws {Error} if a malformed filter is provided. - * * @param {object} options - Address search options. + * @param {boolean=} options.autoPaginate - Have pagination handled + * automatically. Default: true. * @param {number=} options.maxResults - Maximum number of addresses to return. - * @param {object=} options.filter - Search filter. - * @param {string} options.filter.fieldName - Address field to consider in this - * filter. - * @param {string} options.filter.operatorString - Filter operator, can be - * either 'eq' or 'ne'. - * @param {string} options.filter.literalString - String to compare the address - * field to. Can be a regular expression. + * @param {string=} options.filter - Search filter. The filter must contain: + * `FIELD_NAME COMPARISON_STRING LITERAL_STRING`. `FIELD_NAME` is the name + * of the field to compare. `COMPARISON` is the comparison operator that can + * be either `eq` or `ne`. `LITERAL_STRING` is the string to filter to. + * For string fields `LITERAL_STRING` can be a regular expression. * @param {string=} pageToken - Page identifier used in paginated search. * @param {function} callback - The callback function. * @@ -151,71 +157,78 @@ Region.prototype.createAddress = function(name, callback) { * // `addresses` is an array of `Address` objects. * }; * - * var compute = gcloud.compute({ - * projectId: 'grape-spaceship-123' - * }); - * * var myRegion = compute.region('region-name'); * * myRegion.getAddresses( * { - * filter: { - * fieldName : 'name', - * operatorString : 'eq', - * literalString : 'address-[0-9]' - * }], + * filter: 'name eq address-[0-9]' * }, callback); + * + * //- + * // To control how many API requests are made and page through the results + * // manually, set `autoPaginate` to `false`. + * //- + * var callback = function(err, addresses, nextQuery, apiResponse) { + * if (nextQuery) { + * // More results exist. + * myRegion.getAddresses(nextQuery, callback); + * } + * }; + * + * myRegion.getAddresses({ + * autoPaginate: false + * }, callback); + * + * //- + * // Get the addresses from your project as a readable object stream. + * //- + * myRegion.getAddresses() + * .on('error', console.error) + * .on('data', function(address) { + * // `address` is an `Address` object. + * }) + * .on('end', function() { + * // All addresses retrieved. + * }); + * + * //- + * // If you anticipate many results, you can end a stream early to prevent + * // unnecessary processing and API requests. + * //- + * myRegion.getAddresses() + * .on('data', function(address) { + * this.end(); + * }); */ Region.prototype.getAddresses = function(options, callback) { - if (!callback) { + if (util.is(options, 'function')) { callback = options; + options = {}; } - var query = {}; - - if (options) { - if (util.is(options.maxResults, 'number')) { - query.maxResults = options.maxResults; - } - if (options.filter) { - if (!util.is(options.filter.fieldName, 'string') || - !util.is(options.filter.operatorString, 'string') || - !util.is(options.filter.literalString, 'string')) { - throw new Error( - 'A filter must have name, comparisonString and literal fields.'); - } - if (options.filter.operatorString !== 'eq' && - options.filter.operatorString !== 'ne') { - throw new Error('Filter operator must be either eq or ne'); - } - query.filter = - options.filter.fieldName + ' ' + - options.filter.operatorString + ' \'' + - options.filter.literalString + '\''; - } - if (util.is(options.pageToken, 'string')) { - query.pageToken = options.pageToken; - } - } + options = options || {}; var self = this; - this.makeReq_('GET', '/addresses', query, null, function(err, resp) { + this.makeReq_('GET', '/addresses', options, null, function(err, resp) { if (err) { callback(err); return; } - if (resp.items) { - var addresses = []; - for (var i = 0; i < resp.items.length; i++) { - var metadata = resp.items[i]; - var address = self.address(metadata.name); - address.metadata = metadata; - addresses.push(address); - } - callback(null, addresses); - } else { - callback(null, []); + + var nextQuery = null; + + if (resp.nextPageToken) { + nextQuery = extend({}, options, { + pageToken: resp.nextPageToken + }); } + + var addresses = (resp.items || []).map(function(addressObject) { + var address = self.address(addressObject.name); + address.metadata = addressObject; + return address; + }); + callback(null, addresses, nextQuery, resp); }); }; @@ -236,4 +249,11 @@ Region.prototype.makeReq_ = function(method, path, query, body, callback) { this.compute.makeReq_(method, path, query, body, callback); }; +/*! Developer Documentation + * + * These methods can be used with either a callback or as a readable object + * stream. `streamRouter` is used to add this dual behavior. + */ +streamRouter.extend(Region, ['getAddresses']); + module.exports = Region; diff --git a/lib/compute/zone.js b/lib/compute/zone.js index 93fac0fa7db..f70ea7a4cd5 100644 --- a/lib/compute/zone.js +++ b/lib/compute/zone.js @@ -20,6 +20,8 @@ 'use strict'; +var extend = require('extend'); + /** * @type {module:compute/instance} * @private @@ -38,6 +40,12 @@ var Disk = require('./disk.js'); */ var util = require('../common/util.js'); +/** + * @type {module:common/streamrouter} + * @private + */ +var streamRouter = require('../common/stream-router.js'); + /** * Create a Zone object to interact with a Google Compute Engine zone. * @@ -46,8 +54,8 @@ var util = require('../common/util.js'); * * @throws {Error} if a zone name is not provided. * - * @param {module:compute} compute - The Google Compute Engine instance this - * disk belongs to. + * @param {module:compute} compute - The Google Compute Engine object this + * zone belongs to. * @param {string} name - Name of the zone. * * @example @@ -267,17 +275,15 @@ Zone.prototype.createInstance = function(name, options, callback) { /** * Get a list of instances in this zone. * - * @throws {Error} if a malformed filter is provided. - * * @param {object} options - Instance search options. + * @param {boolean=} options.autoPaginate - Have pagination handled + * automatically. Default: true. * @param {number=} options.maxResults - Maximum number of instances to return. - * @param {object=} options.filter - Search filter. - * @param {string} options.filter.fieldName - Instance field to consider in this - * filter. - * @param {string} options.filter.operatorString - Filter operator, can be - * either 'eq' or 'ne'. - * @param {string} options.filter.literalString - String to compare the instance - * field to. Can be a regular expression. + * @param {string=} options.filter - Search filter. The filter must contain: + * `FIELD_NAME COMPARISON_STRING LITERAL_STRING`. `FIELD_NAME` is the name + * of the field to compare. `COMPARISON` is the comparison operator that can + * be either `eq` or `ne`. `LITERAL_STRING` is the string to filter to. + * For string fields `LITERAL_STRING` can be a regular expression. * @param {string=} pageToken - Page identifier used in paginated search. * @param {function} callback - The callback function. * @@ -286,20 +292,48 @@ Zone.prototype.createInstance = function(name, options, callback) { * // `instances` is an array of `Instance` objects. * }; * - * var compute = gcloud.compute({ - * projectId: 'grape-spaceship-123' - * }); - * * var myZone = compute.zone('zone-name'); * * myZone.getInstances( * { - * filter: { - * fieldName : 'name', - * operatorString : 'eq', - * literalString : 'instance-[0-9]' - * }], + * filter: 'name eq instance-[0-9]' * }, callback); + * + * //- + * // To control how many API requests are made and page through the results + * // manually, set `autoPaginate` to `false`. + * //- + * var callback = function(err, instances, nextQuery, apiResponse) { + * if (nextQuery) { + * // More results exist. + * myZone.getInstances(nextQuery, callback); + * } + * }; + * + * myZone.getInstances({ + * autoPaginate: false + * }, callback); + * + * //- + * // Get the instances from your project as a readable object stream. + * //- + * myZone.getInstances() + * .on('error', console.error) + * .on('data', function(instance) { + * // `instance` is an `Instance` object. + * }) + * .on('end', function() { + * // All instances retrieved. + * }); + * + * //- + * // If you anticipate many results, you can end a stream early to prevent + * // unnecessary processing and API requests. + * //- + * myZone.getInstances() + * .on('data', function(instance) { + * this.end(); + * }); */ Zone.prototype.getInstances = function(options, callback) { if (util.is(options, 'function')) { @@ -307,49 +341,29 @@ Zone.prototype.getInstances = function(options, callback) { options = {}; } - var query = {}; - - if (util.is(options.maxResults, 'number')) { - query.maxResults = options.maxResults; - } - if (options.filter) { - if (!util.is(options.filter.fieldName, 'string') || - !util.is(options.filter.operatorString, 'string') || - !util.is(options.filter.literalString, 'string')) { - throw new Error( - 'A filter must have name, comparisonString and literal fields.'); - } - if (options.filter.operatorString !== 'eq' && - options.filter.operatorString !== 'ne') { - throw new Error('Filter operator must be either eq or ne'); - } - query.filter = - options.filter.fieldName + ' ' + - options.filter.operatorString + ' \'' + - options.filter.literalString + '\''; - } - if (util.is(options.pageToken, 'string')) { - query.pageToken = options.pageToken; - } + options = options || {}; var self = this; - this.makeReq_('GET', '/instances', query, null, function(err, resp) { + this.makeReq_('GET', '/instances', options, null, function(err, resp) { if (err) { callback(err); return; } - if (resp.items) { - var instances = []; - for (var i = 0; i < resp.items.length; i++) { - var metadata = resp.items[i]; - var instance = self.instance(metadata.name); - instance.metadata = metadata; - instances.push(instance); - } - callback(null, instances); - } else { - callback(null, []); + + var nextQuery = null; + + if (resp.nextPageToken) { + nextQuery = extend({}, options, { + pageToken: resp.nextPageToken + }); } + + var instances = (resp.items || []).map(function(instanceObject) { + var instance = self.instance(instanceObject.name); + instance.metadata = instanceObject; + return instance; + }); + callback(null, instances, nextQuery, resp); }); }; @@ -432,17 +446,15 @@ Zone.prototype.createDisk = function(name, options, callback) { /** * Get a list of disks in this zone. * - * @throws {Error} if a malformed filter is provided. - * * @param {object} options - Disk search options. + * @param {boolean=} options.autoPaginate - Have pagination handled + * automatically. Default: true. * @param {number=} options.maxResults - Maximum number of disks to return. - * @param {object=} options.filter - Search filter. - * @param {string} options.filter.fieldName - Disk field to consider in this - * filter. - * @param {string} options.filter.operatorString - Filter operator, can be - * either 'eq' or 'ne'. - * @param {string} options.filter.literalString - String to compare the disk - * field to. Can be a regular expression. + * @param {string=} options.filter - Search filter. The filter must contain: + * `FIELD_NAME COMPARISON_STRING LITERAL_STRING`. `FIELD_NAME` is the name + * of the field to compare. `COMPARISON` is the comparison operator that can + * be either `eq` or `ne`. `LITERAL_STRING` is the string to filter to. + * For string fields `LITERAL_STRING` can be a regular expression. * @param {string=} pageToken - Page identifier used in paginated search. * @param {function} callback - The callback function. * @@ -451,20 +463,48 @@ Zone.prototype.createDisk = function(name, options, callback) { * // `disks` is an array of `Disk` objects. * }; * - * var compute = gcloud.compute({ - * projectId: 'grape-spaceship-123' - * }); - * * var myZone = compute.zone('zone-name'); * * myZone.getDisks( * { - * filter: { - * fieldName : 'name', - * operatorString : 'eq', - * literalString : 'disk-[0-9]' - * }], + * filter: 'name eq disk-[0-9]' * }, callback); + * + * //- + * // To control how many API requests are made and page through the results + * // manually, set `autoPaginate` to `false`. + * //- + * var callback = function(err, disks, nextQuery, apiResponse) { + * if (nextQuery) { + * // More results exist. + * myZone.getDisks(nextQuery, callback); + * } + * }; + * + * myZone.getDisks({ + * autoPaginate: false + * }, callback); + * + * //- + * // Get the disks from your project as a readable object stream. + * //- + * myZone.getDisks() + * .on('error', console.error) + * .on('data', function(disk) { + * // `disk` is an `Disk` object. + * }) + * .on('end', function() { + * // All disks retrieved. + * }); + * + * //- + * // If you anticipate many results, you can end a stream early to prevent + * // unnecessary processing and API requests. + * //- + * myZone.getDisks() + * .on('data', function(disk) { + * this.end(); + * }); */ Zone.prototype.getDisks = function(options, callback) { if (util.is(options, 'function')) { @@ -472,49 +512,29 @@ Zone.prototype.getDisks = function(options, callback) { options = {}; } - var query = {}; - - if (util.is(options.maxResults, 'number')) { - query.maxResults = options.maxResults; - } - if (options.filter) { - if (!util.is(options.filter.fieldName, 'string') || - !util.is(options.filter.operatorString, 'string') || - !util.is(options.filter.literalString, 'string')) { - throw new Error( - 'A filter must have name, comparisonString and literal fields.'); - } - if (options.filter.operatorString !== 'eq' && - options.filter.operatorString !== 'ne') { - throw new Error('Filter operator must be either eq or ne'); - } - query.filter = - options.filter.fieldName + ' ' + - options.filter.operatorString + ' \'' + - options.filter.literalString + '\''; - } - if (util.is(options.pageToken, 'string')) { - query.pageToken = options.pageToken; - } + options = options || {}; var self = this; - this.makeReq_('GET', '/disks', query, null, function(err, resp) { + this.makeReq_('GET', '/disks', options, null, function(err, resp) { if (err) { callback(err); return; } - if (resp.items) { - var disks = []; - for (var i = 0; i < resp.items.length; i++) { - var metadata = resp.items[i]; - var disk = self.disk(metadata.name); - disk.metadata = metadata; - disks.push(disk); - } - callback(null, disks); - } else { - callback(null, []); + + var nextQuery = null; + + if (resp.nextPageToken) { + nextQuery = extend({}, options, { + pageToken: resp.nextPageToken + }); } + + var disks = (resp.items || []).map(function(diskObject) { + var disk = self.disk(diskObject.name); + disk.metadata = diskObject; + return disk; + }); + callback(null, disks, nextQuery, resp); }); }; @@ -535,4 +555,11 @@ Zone.prototype.makeReq_ = function(method, path, query, body, callback) { this.compute.makeReq_(method, path, query, body, callback); }; +/*! Developer Documentation + * + * These methods can be used with either a callback or as a readable object + * stream. `streamRouter` is used to add this dual behavior. + */ +streamRouter.extend(Zone, ['getInstances', 'getDisks']); + module.exports = Zone; \ No newline at end of file From 256bcb95d1eb27ae924981a7b0213602667fbe6d Mon Sep 17 00:00:00 2001 From: Marco Ziccardi Date: Tue, 28 Jul 2015 17:25:22 +0200 Subject: [PATCH 7/9] Compute: add references to API docs when necessary --- lib/compute/firewall.js | 3 ++- lib/compute/index.js | 18 ++++++++++++------ lib/compute/region.js | 6 ++++-- lib/compute/zone.js | 12 ++++++++---- 4 files changed, 26 insertions(+), 13 deletions(-) diff --git a/lib/compute/firewall.js b/lib/compute/firewall.js index a11b1d3a568..6153649a13c 100644 --- a/lib/compute/firewall.js +++ b/lib/compute/firewall.js @@ -112,7 +112,8 @@ Firewall.prototype.getMetadata = function(callback) { * * @param {module:compute} compute - The Google Compute Engine instance this * this firewall belongs to. - * @param {object} metadata - Firewall options. + * @param {object} metadata - Firewall metadata. See a + * [Firewall resource](https://goo.gl/7FpjXA) for detailed information. * @param {string=} metadata.network - Network to which the firewall applies. * Default value is 'global/networks/default'. * @param {object[]=} metadata.allowed - List of allowed protocols and ports. diff --git a/lib/compute/index.js b/lib/compute/index.js index 0ca7321253f..d073dc01efd 100644 --- a/lib/compute/index.js +++ b/lib/compute/index.js @@ -194,7 +194,8 @@ Compute.prototype.snapshot = function(name) { }; /** - * Create a firewall rule. + * Create a firewall rule. For a detailed description of method's options see + * [API reference](https://goo.gl/kTMHep). * * @throws {Error} if a firewall name or firewall options are not provided. * If allowed ports, source tags or source ranges are not provided. @@ -291,7 +292,8 @@ Compute.prototype.createFirewall = function(name, options, callback) { }; /** - * Get a list of instances. + * Get a list of instances. For a detailed description of method's options see + * [API reference](https://goo.gl/GeDAwy). * * @param {object} options - Instance search options. * @param {boolean=} options.autoPaginate - Have pagination handled @@ -394,7 +396,8 @@ Compute.prototype.getInstances = function(options, callback) { }; /** - * Get a list of disks. + * Get a list of disks. For a detailed description of method's options see + * [API reference](https://goo.gl/M9Qjb3). * * @param {object} options - Disk search options. * @param {boolean=} options.autoPaginate - Have pagination handled @@ -493,7 +496,8 @@ Compute.prototype.getDisks = function(options, callback) { }; /** - * Get a list of addresses. + * Get a list of addresses. For a detailed description of method's options see + * [API reference](https://goo.gl/r9XmXJ). * * @param {object} options - Address search options. * @param {boolean=} options.autoPaginate - Have pagination handled @@ -596,7 +600,8 @@ Compute.prototype.getAddresses = function(options, callback) { }; /** - * Get a list of snapshots. + * Get a list of snapshots. For a detailed description of method's options see + * [API reference](https://goo.gl/IEMVgi). * * @param {object} options - Snapshot search options. * @param {boolean=} options.autoPaginate - Have pagination handled @@ -689,7 +694,8 @@ Compute.prototype.getSnapshots = function(options, callback) { }; /** - * Get a list of firewall rules. + * Get a list of firewall rules. For a detailed description of method's options + * see [API reference](https://goo.gl/TZRxht). * * @param {object} options - Firewall search options. * @param {boolean=} options.autoPaginate - Have pagination handled diff --git a/lib/compute/region.js b/lib/compute/region.js index 592f8a78bd9..54a298d957b 100644 --- a/lib/compute/region.js +++ b/lib/compute/region.js @@ -92,7 +92,8 @@ Region.prototype.address = function(name) { }; /** - * Create an address in this region. + * Create an address in this region. For a detailed description of method's + * options see [API reference](https://goo.gl/lY8Y3u). * * @throws {Error} if an address name or a callback are not provided. * @@ -138,7 +139,8 @@ Region.prototype.createAddress = function(name, callback) { }; /** - * Get a list of addresses in this region. + * Get a list of addresses in this region. For a detailed description of + * method's options see [API reference](https://goo.gl/By1Az6). * * @param {object} options - Address search options. * @param {boolean=} options.autoPaginate - Have pagination handled diff --git a/lib/compute/zone.js b/lib/compute/zone.js index f70ea7a4cd5..5ade3e18301 100644 --- a/lib/compute/zone.js +++ b/lib/compute/zone.js @@ -119,7 +119,8 @@ Zone.prototype.disk = function(name) { }; /** - * Create an instance in the zone. + * Create an instance in the zone. For a detailed description of method's + * options see [API reference](https://goo.gl/oWcGvQ). * * @throws {Error} if an instance name or options are not provided. * @@ -273,7 +274,8 @@ Zone.prototype.createInstance = function(name, options, callback) { }; /** - * Get a list of instances in this zone. + * Get a list of instances in this zone. For a detailed description of method's + * options see [API reference](https://goo.gl/80ya6l). * * @param {object} options - Instance search options. * @param {boolean=} options.autoPaginate - Have pagination handled @@ -368,7 +370,8 @@ Zone.prototype.getInstances = function(options, callback) { }; /** - * Create a disk in this zone. + * Create a disk in this zone. For a detailed description of method's options + * see [API reference](https://goo.gl/suU3qn). * * @throws {Error} if a disk name or options are not provided or if both a * source image and a source snapshot are provided. @@ -444,7 +447,8 @@ Zone.prototype.createDisk = function(name, options, callback) { }; /** - * Get a list of disks in this zone. + * Get a list of disks in this zone. For a detailed description of method's + * options see [API reference](https://goo.gl/0R67mp). * * @param {object} options - Disk search options. * @param {boolean=} options.autoPaginate - Have pagination handled From 4ebb6b0510b30ee64977d0df91ebc60f63d23575 Mon Sep 17 00:00:00 2001 From: Marco Ziccardi Date: Wed, 29 Jul 2015 21:03:52 +0200 Subject: [PATCH 8/9] Compute: add Network support, minor fixes to docs --- lib/compute/index.js | 226 +++++++++++++++++++++++++++++++++------ lib/compute/network.js | 230 ++++++++++++++++++++++++++++++++++++++++ lib/compute/snapshot.js | 4 +- lib/compute/zone.js | 2 +- 4 files changed, 424 insertions(+), 38 deletions(-) create mode 100644 lib/compute/network.js diff --git a/lib/compute/index.js b/lib/compute/index.js index d073dc01efd..738d425aad2 100644 --- a/lib/compute/index.js +++ b/lib/compute/index.js @@ -46,6 +46,12 @@ var Firewall = require('./firewall.js'); */ var Snapshot = require('./snapshot.js'); +/** + * @type {module:compute/firewall} + * @private + */ +var Network = require('./network.js'); + /** * @type {module:common/util} * @private @@ -193,6 +199,25 @@ Compute.prototype.snapshot = function(name) { return new Snapshot(this, name); }; +/** + * Get a reference to a Google Compute Engine network. + * + * @param {string} name - Name of the existing network. + * @return {module:compute/network} + * + * @example + * var gcloud = require('gcloud')({ + * keyFilename: '/path/to/keyfile.json' + * }); + * + * var compute = gcloud.compute(); + * + * var network = compute.network('network-name'); + */ +Compute.prototype.network = function(name) { + return new Network(this, name); +}; + /** * Create a firewall rule. For a detailed description of method's options see * [API reference](https://goo.gl/kTMHep). @@ -200,13 +225,11 @@ Compute.prototype.snapshot = function(name) { * @throws {Error} if a firewall name or firewall options are not provided. * If allowed ports, source tags or source ranges are not provided. * - * @param {module:compute} compute - The Google Compute Engine object this - * firewall belongs to. * @param {string} name - Name of the firewall. * @param {object} options - Firewall options. * @param {string=} options.description - Description of the firewall. - * @param {string=} options.network - Network to which the firewall applies. - * Default value is 'global/networks/default'. + * @param {string=|module:compute/network} options.network - Network to which + * the firewall applies. Default value is 'global/networks/default'. * @param {object[]=} options.allowed - List of allowed protocols and ports. * @param {string[]=} options.sourceRanges - IP address blocks to which this * rule applies (in CIDR format). @@ -252,45 +275,84 @@ Compute.prototype.createFirewall = function(name, options, callback) { throw new Error('Source ranges or source tags must be provided.'); } - var body = {}; - body.name = name; - if (util.is(options.description, 'string')) { - body.description = options.description; - } - if (util.is(options.network, 'string')) { - body.network = options.network; + if (util.is(options.network, 'object')) { + options.network = + 'projects/' + + options.network.compute.projectId + + '/global/networks/' + + options.network.name; } else { - body.network = 'global/networks/default'; - } - if (util.is(options.allowed, 'array')) { - body.allowed = options.allowed; - } - if (util.is(options.sourceRanges, 'array')) { - body.sourceRanges = options.sourceRanges; - } - if (util.is(options.sourceTags, 'array')) { - body.sourceTags = options.sourceTags; - } - if (util.is(options.targetTags, 'array')) { - body.targetTags = options.targetTags; + options.network = options.network || 'global/networks/default'; } + options.name = name; + var self = this; - this.makeReq_( - 'POST', + this.makeReq_('POST', '/global/firewalls', null, - body, function(err, resp) { + options, function(err, resp) { if (err) { callback(err); return; } var firewall = self.firewall(name); - firewall.name = options; + firewall.metadata = options; callback(null, firewall, resp); }); }; +/** + * Create a network. For a detailed description of method's options see + * [API reference](https://goo.gl/cWYdER). + * + * @throws {Error} if a network name or an IPv4 range is not provided. + * + * @param {string} name - Name of the network. + * @param {object} options - Network options. + * @param {string} options.IPv4Range - CIDR range of addresses that are legal on + * this network. + * @param {string=} options.description - Description of the network. + * @param {function} callback - The callback function. + * + * @example + * var callback = function(err, network, apiResponse) { + * // `network` is a Network object. + * }; + * + * var compute = gcloud.compute({ + * projectId: 'grape-spaceship-123' + * }); + * + * var network = compute.createNetwork('network', + * { + * IPv4Range: '192.168.0.0/16' + * }, callback); + */ +Compute.prototype.createNetwork = function(name, options, callback) { + if (!name) { + throw new Error('A name is needed to use a Compute Engine Network.'); + } else if (!/^(?:[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?)$/.test(name)) { + throw new Error('Name must match [a-z]([-a-z0-9]{0,61}[a-z0-9])?'); + } + if (!options || !util.is(options.IPv4Range, 'string')) { + throw new Error('An IPv4 range must be provided.'); + } + + options.name = name; + + var self = this; + this.makeReq_('POST', '/global/networks', null, options, function(err, resp) { + if (err) { + callback(err); + return; + } + var network = self.network(name); + network.metadata = options; + callback(null, network, resp); + }); +}; + /** * Get a list of instances. For a detailed description of method's options see * [API reference](https://goo.gl/GeDAwy). @@ -442,7 +504,7 @@ Compute.prototype.getInstances = function(options, callback) { * compute.getDisks() * .on('error', console.error) * .on('data', function(disk) { - * // `disk` is an `Disk` object. + * // `disk` is a `Disk` object. * }) * .on('end', function() { * // All disks retrieved. @@ -646,7 +708,7 @@ Compute.prototype.getAddresses = function(options, callback) { * compute.getSnapshots() * .on('error', console.error) * .on('data', function(snapshot) { - * // `snapshot` is an `Snapshot` object. + * // `snapshot` is a `Snapshot` object. * }) * .on('end', function() { * // All snapshots retrieved. @@ -716,7 +778,6 @@ Compute.prototype.getSnapshots = function(options, callback) { * * compute.getFirewalls( * { - * zone: 'europe-west1-b', * filter: 'name eq firewall-[0-9]' * }, callback); * @@ -736,12 +797,12 @@ Compute.prototype.getSnapshots = function(options, callback) { * }, callback); * * //- - * // Get the snapshots from your project as a readable object stream. + * // Get the firewalls from your project as a readable object stream. * //- - * compute.getSnapshots() + * compute.getFirewalls() * .on('error', console.error) * .on('data', function(firewall) { - * // `firewall` is an `Firewall` object. + * // `firewall` is a `Firewall` object. * }) * .on('end', function() { * // All firewalls retrieved. @@ -788,6 +849,100 @@ Compute.prototype.getFirewalls = function(options, callback) { }); }; +/** + * Get a list of networks. For a detailed description of method's options + * see [API reference](https://goo.gl/yx70Gc). + * + * @param {object} options - Network search options. + * @param {boolean=} options.autoPaginate - Have pagination handled + * automatically. Default: true. + * @param {number=} options.maxResults - Maximum number of networks to return. + * @param {string=} options.filter - Search filter. The filter must contain: + * `FIELD_NAME COMPARISON_STRING LITERAL_STRING`. `FIELD_NAME` is the name + * of the field to compare. `COMPARISON` is the comparison operator that can + * be either `eq` or `ne`. `LITERAL_STRING` is the string to filter to. + * For string fields `LITERAL_STRING` can be a regular expression. + * @param {string=} pageToken - Page identifier used in paginated search. + * @param {function} callback - The callback function. + * + * @example + * var callback = function(err, networks) { + * // `networks` is an array of `Network` objects. + * }; + * + * compute.getNetworks( + * { + * filter: 'name eq network-[0-9]' + * }, callback); + * + * //- + * // To control how many API requests are made and page through the results + * // manually, set `autoPaginate` to `false`. + * //- + * var callback = function(err, networks, nextQuery, apiResponse) { + * if (nextQuery) { + * // More results exist. + * compute.getNetworks(nextQuery, callback); + * } + * }; + * + * compute.getNetworks({ + * autoPaginate: false + * }, callback); + * + * //- + * // Get the networks from your project as a readable object stream. + * //- + * compute.getNetworks() + * .on('error', console.error) + * .on('data', function(network) { + * // `network` is a `Network` object. + * }) + * .on('end', function() { + * // All networks retrieved. + * }); + * + * //- + * // If you anticipate many results, you can end a stream early to prevent + * // unnecessary processing and API requests. + * //- + * compute.getNetworks() + * .on('data', function(network) { + * this.end(); + * }); + */ +Compute.prototype.getNetworks = function(options, callback) { + if (util.is(options, 'function')) { + callback = options; + options = {}; + } + + options = options || {}; + + var self = this; + this.makeReq_('GET', '/global/networks', options, null, function(err, resp) { + if (err) { + callback(err); + return; + } + + var nextQuery = null; + + if (resp.nextPageToken) { + nextQuery = extend({}, options, { + pageToken: resp.nextPageToken + }); + } + + var networks = (resp.items || []).map(function(networkObject) { + var network = self.network(networkObject.name); + network.metadata = networkObject; + return network; + }); + callback(null, networks, nextQuery, resp); + }); +}; + /** * Make a new request object from the provided arguments and wrap the callback * to intercept non-successful responses. @@ -824,7 +979,8 @@ streamRouter.extend(Compute, [ 'getDisks', 'getAddresses', 'getSnapshots', - 'getFirewalls' + 'getFirewalls', + 'getNetworks' ]); module.exports = Compute; diff --git a/lib/compute/network.js b/lib/compute/network.js new file mode 100644 index 00000000000..d7c2e2fd50a --- /dev/null +++ b/lib/compute/network.js @@ -0,0 +1,230 @@ +/*! + * Copyright 2014 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*! + * @module compute/network + */ + +'use strict'; + +/** + * @type {module:common/util} + * @private + */ +var util = require('../common/util.js'); + +/** + * Create a Network object to interact with a Google Compute Engine network. + * + * @constructor + * @alias module:compute/network + * + * @throws {Error} if a network name is not provided. + * + * @param {module:compute} compute - The Compute module this network belongs to. + * @param {string} name - Network name. + * @param {object=} metadata - Network metadata. + * + * @example + * var gcloud = require('gcloud')({ + * keyFilename: '/path/to/keyfile.json' + * }); + * + * var compute = gcloud.compute(); + * + * var network = compute.network('network-name'); + */ +function Network(compute, name, metadata) { + this.compute = compute; + this.name = name; + this.metadata = metadata; + + if (!util.is(this.name, 'string')) { + throw new Error('A name is needed to use a Compute Engine Network.'); + } +} + +/** + * Delete the network. + * + * @param {function} callback - The callback function. + * + * @example + * network.delete(function(err) {}); + */ +Network.prototype.delete = function(callback) { + callback = callback || util.noop; + this.makeReq_('DELETE', '', null, true, callback); +}; + +/** + * Get the network's metadata. + * + * @param {function=} callback - The callback function. + * + * @example + * network.getMetadata(function(err, metadata, apiResponse) {}); + */ +Network.prototype.getMetadata = function(callback) { + callback = callback || util.noop; + var self = this; + this.makeReq_('GET', '', null, null, function(err, resp) { + if (err) { + callback(err); + return; + } + self.metadata = resp; + callback(null, self.metadata, resp); + }); +}; + +/** + * Create a firewall rule for this network. For a detailed description of + * method's options see [API reference](https://goo.gl/kTMHep). + * + * @throws {Error} if a firewall name is not provided. If allowed ports, + * source tags or source ranges are not provided. + * + * @param {string} name - Name of the firewall. + * @param {object} options - Firewall options. + * @param {string=} options.description - Description of the firewall. + * @param {object[]=} options.allowed - List of allowed protocols and ports. + * @param {string[]=} options.sourceRanges - IP address blocks to which this + * rule applies (in CIDR format). + * @param {string[]=} options.sourceRanges - IP address blocks to which this + * rule applies (in CIDR format). + * @param {string[]=} options.sourceTags - List of instance tags to which + * this rule applies. + * @param {string[]=} options.targetTags - List of instance tags indicating + * instances that may process connections according to this rule. + * @param {function} callback - The callback function. + * + * @example + * var network = compute.network('network-name'); + * + * var firewall = network.createFirewall('tcp-3000', + * { + * description: 'yada yada', + * allowed: [{ + * IPProtocol: 'tcp', + * ports: ['3000'] + * }], + * sourceRanges: ['0.0.0.0/0'], + * targetTags: ['tcp-3000-tag'] + * }, callback); + */ +Network.prototype.createFirewall = function(name, options, callback) { + options = options || {}; + options.network = + 'projects/' + + this.compute.projectId + + '/global/networks/' + + this.name; + this.compute.createFirewall(name, options, callback); +}; + +/** + * Get a list of firewall rules for this network. + * + * @param {object} options - Firewall search options. + * @param {boolean=} options.autoPaginate - Have pagination handled + * automatically. Default: true. + * @param {number=} options.maxResults - Maximum number of firewalls to return. + * @param {string=} pageToken - Page identifier used in paginated search. + * @param {function} callback - The callback function. + * + * @example + * var callback = function(err, firewalls) { + * // `firewalls` is an array of `Firewall` objects. + * }; + * + * compute.getFirewalls(callback); + * + * //- + * // To control how many API requests are made and page through the results + * // manually, set `autoPaginate` to `false`. + * //- + * var callback = function(err, firewalls, nextQuery, apiResponse) { + * if (nextQuery) { + * // More results exist. + * compute.getFirewalls(nextQuery, callback); + * } + * }; + * + * compute.getFirewalls({ + * autoPaginate: false + * }, callback); + * + * //- + * // Get the firewalls from your project as a readable object stream. + * //- + * compute.getFirewalls() + * .on('error', console.error) + * .on('data', function(firewall) { + * // `firewall` is an `Firewall` object. + * }) + * .on('end', function() { + * // All firewalls retrieved. + * }); + * + * //- + * // If you anticipate many results, you can end a stream early to prevent + * // unnecessary processing and API requests. + * //- + * compute.getFirewalls() + * .on('data', function(firewall) { + * this.end(); + * }); + */ +Network.prototype.getFirewalls = function(options, callback) { + if (util.is(options, 'function')) { + callback = options; + options = {}; + } + + options = options || {}; + options.filter = + 'network eq ' + + '.*projects/' + + this.compute.projectId + + '/global/networks/' + + this.name; + + if (callback) { + this.compute.getFirewalls(options, callback); + } else { + return this.compute.getFirewalls(options); + } +}; + +/** + * Make a new request object from the provided arguments and wrap the callback + * to intercept non-successful responses. + * + * @private + * + * @param {string} method - Action. + * @param {string} path - Request path. + * @param {*} query - Request query object. + * @param {*} body - Request body contents. + * @param {function} callback - The callback function. + */ +Network.prototype.makeReq_ = function(method, path, query, body, callback) { + path = '/global/networks/' + this.name + path; + this.compute.makeReq_(method, path, query, body, callback); +}; + +module.exports = Network; \ No newline at end of file diff --git a/lib/compute/snapshot.js b/lib/compute/snapshot.js index 57a0848f1fc..78191bff5f8 100644 --- a/lib/compute/snapshot.js +++ b/lib/compute/snapshot.js @@ -15,7 +15,7 @@ */ /*! - * @module compute/disk + * @module compute/snapshot */ 'use strict'; @@ -46,7 +46,7 @@ var util = require('../common/util.js'); * * var compute = gcloud.compute(); * - * var disk = compute.snapshot('snapshot-name'); + * var snapshot = compute.snapshot('snapshot-name'); */ function Snapshot(compute, name, metadata) { this.compute = compute; diff --git a/lib/compute/zone.js b/lib/compute/zone.js index 5ade3e18301..772337aee0b 100644 --- a/lib/compute/zone.js +++ b/lib/compute/zone.js @@ -495,7 +495,7 @@ Zone.prototype.createDisk = function(name, options, callback) { * myZone.getDisks() * .on('error', console.error) * .on('data', function(disk) { - * // `disk` is an `Disk` object. + * // `disk` is a `Disk` object. * }) * .on('end', function() { * // All disks retrieved. From 0b7c0b0af7c24e7e08199900f14c6a0c30a7fa9b Mon Sep 17 00:00:00 2001 From: Marco Ziccardi Date: Mon, 3 Aug 2015 12:17:28 +0200 Subject: [PATCH 9/9] Compute: add operation support. Add operation parameter to callbacks of methods wrapping an asynchronous API (in place of APIresponse). --- lib/compute/address.js | 16 ++++- lib/compute/disk.js | 28 ++++++-- lib/compute/firewall.js | 25 +++++-- lib/compute/index.js | 142 ++++++++++++++++++++++++++++++++++++-- lib/compute/instance.js | 123 +++++++++++++++++++++++++++------ lib/compute/network.js | 22 +++++- lib/compute/operation.js | 143 +++++++++++++++++++++++++++++++++++++++ lib/compute/region.js | 137 ++++++++++++++++++++++++++++++++++++- lib/compute/snapshot.js | 16 ++++- lib/compute/zone.js | 143 +++++++++++++++++++++++++++++++++++++-- 10 files changed, 747 insertions(+), 48 deletions(-) create mode 100644 lib/compute/operation.js diff --git a/lib/compute/address.js b/lib/compute/address.js index 9f9cf84b334..a7d4cad33a3 100644 --- a/lib/compute/address.js +++ b/lib/compute/address.js @@ -69,11 +69,23 @@ function Address(region, name, metadata) { * @param {function} callback - The callback function. * * @example - * address.delete(function(err) {}); + * address.delete(function(err, operation) { + * // `operation` is an Operation object and can be used to check the status + * // of address deletion. + * }); */ Address.prototype.delete = function(callback) { callback = callback || util.noop; - this.makeReq_('DELETE', '', null, true, callback); + var region = this.region; + this.makeReq_('DELETE', '', null, true, function(err, resp) { + if (err) { + callback(err); + return; + } + var operation = region.operation(resp.name); + operation.metadata = resp; + callback(null, operation); + }); }; /** diff --git a/lib/compute/disk.js b/lib/compute/disk.js index f16e788656c..6c1a9398961 100644 --- a/lib/compute/disk.js +++ b/lib/compute/disk.js @@ -69,11 +69,23 @@ function Disk(zone, name, metadata) { * @param {function} callback - The callback function. * * @example - * disk.delete(function(err) {}); + * disk.delete(function(err, operation) { + * // `operation` is an Operation object and can be used to check the status + * // of disk deletion. + * }); */ Disk.prototype.delete = function(callback) { callback = callback || util.noop; - this.makeReq_('DELETE', '', null, true, callback); + var zone = this.zone; + this.makeReq_('DELETE', '', null, true, function(err, resp) { + if (err) { + callback(err); + return; + } + var operation = zone.operation(resp.name); + operation.metadata = resp; + callback(null, operation); + }); }; /** @@ -106,8 +118,10 @@ Disk.prototype.getMetadata = function(callback) { * @param {function} callback - The callback function. * * @example - * disk.createSnapshot('my-snapshot', function(err, snapshot, apiResponse) { + * disk.createSnapshot('my-snapshot', function(err, snapshot, operation) { * // `snapshot` is a Snapshot object. + * // `operation` is an Operation object and can be used to check the status + * // of snapshot creation. * }); */ Disk.prototype.createSnapshot = function(options, callback) { @@ -122,14 +136,16 @@ Disk.prototype.createSnapshot = function(options, callback) { throw new Error('Name must match [a-z]([-a-z0-9]{0,61}[a-z0-9])?'); } - var compute = this.zone.compute; + var self = this; this.makeReq_('POST', '/createSnapshot', null, options, function(err, resp) { if (err) { callback(err); return; } - var snapshot = compute.snapshot(options.name); - callback(null, snapshot, resp); + var snapshot = self.zone.compute.snapshot(options.name); + var operation = self.zone.operation(resp.name); + operation.metadata = resp; + callback(null, snapshot, operation); }); }; diff --git a/lib/compute/firewall.js b/lib/compute/firewall.js index 6153649a13c..639ffd32318 100644 --- a/lib/compute/firewall.js +++ b/lib/compute/firewall.js @@ -77,11 +77,23 @@ function Firewall(compute, name, options) { * @param {function} callback - The callback function. * * @example - * firewall.delete(function(err) {}); + * firewall.delete(function(err, operation) { + * // `operation` is an Operation object and can be used to check the status + * // of firewall rule deletion. + * }); */ Firewall.prototype.delete = function(callback) { callback = callback || util.noop; - this.makeReq_('DELETE', '', null, true, callback); + var compute = this.compute; + this.makeReq_('DELETE', '', null, true, function(err, resp) { + if (err) { + callback(err); + return; + } + var operation = compute.operation(resp.name); + operation.metadata = resp; + callback(null, operation); + }); }; /** @@ -128,8 +140,9 @@ Firewall.prototype.getMetadata = function(callback) { * @param {function} callback - The callback function. * * @example - * var callback = function(err, apiResponse) { - * // ... + * var callback = function(err, operation) { + * // `operation` is an Operation object and can be used to check the status + * // of firewall rule update. * }; * * firewall.setMetadata( @@ -153,7 +166,9 @@ Firewall.prototype.setMetadata = function(metadata, callback) { return; } self.metadata = metadata; - callback(null, resp); + var operation = self.compute.operation(resp.name); + operation.metadata = resp; + callback(null, operation); }); }; diff --git a/lib/compute/index.js b/lib/compute/index.js index 738d425aad2..c1d74835ee8 100644 --- a/lib/compute/index.js +++ b/lib/compute/index.js @@ -52,6 +52,12 @@ var Snapshot = require('./snapshot.js'); */ var Network = require('./network.js'); +/** + * @type {module:compute/operation} + * @private + */ +var Operation = require('./operation.js'); + /** * @type {module:common/util} * @private @@ -218,6 +224,25 @@ Compute.prototype.network = function(name) { return new Network(this, name); }; +/** + * Get a reference to a Google Compute Engine operation. + * + * @param {string} name - Name of the existing operation. + * @return {module:compute/operation} + * + * @example + * var gcloud = require('gcloud')({ + * keyFilename: '/path/to/keyfile.json' + * }); + * + * var compute = gcloud.compute(); + * + * var operation = compute.operation('operation-name'); + */ +Compute.prototype.operation = function(name) { + return new Operation(this, name); +}; + /** * Create a firewall rule. For a detailed description of method's options see * [API reference](https://goo.gl/kTMHep). @@ -242,8 +267,10 @@ Compute.prototype.network = function(name) { * @param {function} callback - The callback function. * * @example - * var callback = function(err, firewall, apiResponse) { + * var callback = function(err, firewall, operation) { * // `firewall` is a Firewall object. + * // `operation` is an Operation object and can be used to check the status + * // of firewall rule creation. * }; * * var compute = gcloud.compute({ @@ -298,7 +325,9 @@ Compute.prototype.createFirewall = function(name, options, callback) { } var firewall = self.firewall(name); firewall.metadata = options; - callback(null, firewall, resp); + var operation = self.operation(resp.name); + operation.metadata = resp; + callback(null, firewall, operation); }); }; @@ -316,8 +345,10 @@ Compute.prototype.createFirewall = function(name, options, callback) { * @param {function} callback - The callback function. * * @example - * var callback = function(err, network, apiResponse) { + * var callback = function(err, network, operation) { * // `network` is a Network object. + * // `operation` is an Operation object and can be used to check the status + * // of network creation. * }; * * var compute = gcloud.compute({ @@ -349,7 +380,9 @@ Compute.prototype.createNetwork = function(name, options, callback) { } var network = self.network(name); network.metadata = options; - callback(null, network, resp); + var operation = self.operation(resp.name); + operation.metadata = resp; + callback(null, network, operation); }); }; @@ -943,6 +976,104 @@ Compute.prototype.getNetworks = function(options, callback) { }); }; +/** + * Get a list of global operations. For a detailed description of method's + * options see [API reference](https://goo.gl/gX4C1u). + * + * @param {object} options - Operation search options. + * @param {boolean=} options.autoPaginate - Have pagination handled + * automatically. Default: true. + * @param {number=} options.maxResults - Maximum number of operations to return. + * @param {string=} options.filter - Search filter. The filter must contain: + * `FIELD_NAME COMPARISON_STRING LITERAL_STRING`. `FIELD_NAME` is the name + * of the field to compare. `COMPARISON` is the comparison operator that can + * be either `eq` or `ne`. `LITERAL_STRING` is the string to filter to. + * For string fields `LITERAL_STRING` can be a regular expression. + * @param {string=} pageToken - Page identifier used in paginated search. + * @param {function} callback - The callback function. + * + * @example + * var callback = function(err, operations) { + * // `operations` is an array of `Operation` objects. + * }; + * + * compute.getOperations( + * { + * filter: 'name eq operation-[0-9]' + * }, callback); + * + * //- + * // To control how many API requests are made and page through the results + * // manually, set `autoPaginate` to `false`. + * //- + * var callback = function(err, operations, nextQuery, apiResponse) { + * if (nextQuery) { + * // More results exist. + * compute.getOperations(nextQuery, callback); + * } + * }; + * + * compute.getOperations({ + * autoPaginate: false + * }, callback); + * + * //- + * // Get the operations from your project as a readable object stream. + * //- + * compute.getOperations() + * .on('error', console.error) + * .on('data', function(operation) { + * // `operation` is a `Operation` object. + * }) + * .on('end', function() { + * // All operations retrieved. + * }); + * + * //- + * // If you anticipate many results, you can end a stream early to prevent + * // unnecessary processing and API requests. + * //- + * compute.getOperations() + * .on('data', function(operation) { + * this.end(); + * }); + */ +Compute.prototype.getOperations = function(options, callback) { + if (util.is(options, 'function')) { + callback = options; + options = {}; + } + + options = options || {}; + + var self = this; + this.makeReq_( + 'GET', + '/global/operations', + options, + null, function(err, resp) { + if (err) { + callback(err); + return; + } + + var nextQuery = null; + + if (resp.nextPageToken) { + nextQuery = extend({}, options, { + pageToken: resp.nextPageToken + }); + } + + var operations = (resp.items || []).map(function(operationObject) { + var operation = self.operation(operationObject.name); + operation.metadata = operationObject; + return operation; + }); + callback(null, operations, nextQuery, resp); + }); +}; + /** * Make a new request object from the provided arguments and wrap the callback * to intercept non-successful responses. @@ -980,7 +1111,8 @@ streamRouter.extend(Compute, [ 'getAddresses', 'getSnapshots', 'getFirewalls', - 'getNetworks' + 'getNetworks', + 'getOperations' ]); module.exports = Compute; diff --git a/lib/compute/instance.js b/lib/compute/instance.js index f26bc85d44b..a1a72a86aa4 100644 --- a/lib/compute/instance.js +++ b/lib/compute/instance.js @@ -69,11 +69,23 @@ function Instance(zone, name, metadata) { * @param {function} callback - The callback function. * * @example - * instance.delete(function(err) {}); + * instance.delete(function(err, operation) { + * // `operation` is an Operation object and can be used to check the status + * // of instance deletion. + * }); */ Instance.prototype.delete = function(callback) { callback = callback || util.noop; - this.makeReq_('DELETE', '', null, true, callback); + var zone = this.zone; + this.makeReq_('DELETE', '', null, true, function(err, resp) { + if (err) { + callback(err); + return; + } + var operation = zone.operation(resp.name); + operation.metadata = resp; + callback(null, operation); + }); }; /** @@ -116,12 +128,16 @@ Instance.prototype.getMetadata = function(callback) { * @param {function} callback - The callback function. * * @example + * var callback = function(err, operation) { + * // `operation` is an Operation object and can be used to check the status + * // of disk attachment. + * }; + * * instance.attachDisk( * disk, * { * readOnly : true - * } - * function(err, apiResponse) {}); + * }, callback); */ Instance.prototype.attachDisk = function(disk, options, callback) { if (!disk) { @@ -158,7 +174,16 @@ Instance.prototype.attachDisk = function(disk, options, callback) { body.autoDelete = options.autoDelete; } - this.makeReq_('POST', '/attachDisk', null, body, callback); + var zone = this.zone; + this.makeReq_('POST', '/attachDisk', null, body, function(err, resp) { + if (err) { + callback(err); + return; + } + var operation = zone.operation(resp.name); + operation.metadata = resp; + callback(null, operation); + }); }; /** @@ -168,12 +193,16 @@ Instance.prototype.attachDisk = function(disk, options, callback) { * @param {function} callback - The callback function. * * @example + * var callback = function(err, operation) { + * // `operation` is an Operation object and can be used to check the status + * // of disk detachment. + * }; + * * instance.detachDisk( * disk, * { * readOnly : true - * } - * function(err, apiResponse) {}); + * }, callback); */ Instance.prototype.detachDisk = function(deviceName, callback) { if (!util.is(deviceName, 'string')) { @@ -183,7 +212,16 @@ Instance.prototype.detachDisk = function(deviceName, callback) { var query = {}; query.deviceName = deviceName; - this.makeReq_('POST', '/detachDisk', query, null, callback); + var zone = this.zone; + this.makeReq_('POST', '/detachDisk', query, null, function(err, resp) { + if (err) { + callback(err); + return; + } + var operation = zone.operation(resp.name); + operation.metadata = resp; + callback(null, operation); + }); }; /** @@ -229,11 +267,23 @@ Instance.prototype.getSerialPortOutput = function(port, callback) { * @param {function} callback - The callback function. * * @example - * instance.reset(function(err, apiResponse) {}); + * instance.reset(function(err, operation) { + * // `operation` is an Operation object and can be used to check the status + * // of instance reset. + * }); */ Instance.prototype.reset = function(callback) { callback = callback || util.noop; - this.makeReq_('POST', '/reset', null, null, callback); + var zone = this.zone; + this.makeReq_('POST', '/reset', null, null, function(err, resp) { + if (err) { + callback(err); + return; + } + var operation = zone.operation(resp.name); + operation.metadata = resp; + callback(null, operation); + }); }; /** @@ -242,11 +292,23 @@ Instance.prototype.reset = function(callback) { * @param {function} callback - The callback function. * * @example - * instance.start(function(err, apiResponse) {}); + * instance.start(function(err, operation) { + * // `operation` is an Operation object and can be used to check the status + * // of instance start. + * }); */ Instance.prototype.start = function(callback) { callback = callback || util.noop; - this.makeReq_('POST', '/start', null, null, callback); + var zone = this.zone; + this.makeReq_('POST', '/start', null, null, function(err, resp) { + if (err) { + callback(err); + return; + } + var operation = zone.operation(resp.name); + operation.metadata = resp; + callback(null, operation); + }); }; /** @@ -255,11 +317,23 @@ Instance.prototype.start = function(callback) { * @param {function} callback - The callback function. * * @example - * instance.stop(function(err, apiResponse) {}); + * instance.stop(function(err, operation) { + * // `operation` is an Operation object and can be used to check the status + * // of instance stop. + * }); */ Instance.prototype.stop = function(callback) { callback = callback || util.noop; - this.makeReq_('POST', '/stop', null, null, callback); + var zone = this.zone; + this.makeReq_('POST', '/stop', null, null, function(err, resp) { + if (err) { + callback(err); + return; + } + var operation = zone.operation(resp.name); + operation.metadata = resp; + callback(null, operation); + }); }; /** @@ -274,13 +348,12 @@ Instance.prototype.getTags = function(callback) { if (!callback) { throw new Error('A callback must be provided to get tags'); } - this.makeReq_('GET', '', null, null, function(err, resp) { + this.getMetadata(function(err, metadata) { if (err) { callback(err); return; } - this.metadata = resp; - callback(null, resp.tags.items, resp.tags.fingerprint); + callback(null, metadata.tags.items, metadata.tags.fingerprint); }); }; @@ -296,7 +369,10 @@ Instance.prototype.getTags = function(callback) { * instance.getTags(function(err, tags, fingerprint) { * if (err) throw err; * tags.push('new-tag'); - * instance.setTags(tags, fingerprint, function(err, apiResponse) {}); + * instance.setTags(tags, fingerprint, function(err, operation) { + * // `operation` is an Operation object and can be used to check the status + * // of instance tags set. + * }); * }); */ Instance.prototype.setTags = function(tags, fingerprint, callback) { @@ -317,7 +393,16 @@ Instance.prototype.setTags = function(tags, fingerprint, callback) { body.items = tags; body.fingerprint = fingerprint; - this.makeReq_('POST', '/setTags', null, body, callback); + var zone = this.zone; + this.makeReq_('POST', '/setTags', null, body, function(err, resp) { + if (err) { + callback(err); + return; + } + var operation = zone.operation(resp.name); + operation.metadata = resp; + callback(null, operation); + }); }; /** diff --git a/lib/compute/network.js b/lib/compute/network.js index d7c2e2fd50a..658e234b377 100644 --- a/lib/compute/network.js +++ b/lib/compute/network.js @@ -63,11 +63,23 @@ function Network(compute, name, metadata) { * @param {function} callback - The callback function. * * @example - * network.delete(function(err) {}); + * network.delete(function(err, operation) { + * // `operation` is an Operation object and can be used to check the status + * // of network deletion. + * }); */ Network.prototype.delete = function(callback) { callback = callback || util.noop; - this.makeReq_('DELETE', '', null, true, callback); + var compute = this.compute; + this.makeReq_('DELETE', '', null, true, function(err, resp) { + if (err) { + callback(err); + return; + } + var operation = compute.operation(resp.name); + operation.metadata = resp; + callback(null, operation); + }); }; /** @@ -113,6 +125,12 @@ Network.prototype.getMetadata = function(callback) { * @param {function} callback - The callback function. * * @example + * var callback = function(err, firewall, operation) { + * // `firewall` is a Firewall object. + * // `operation` is an Operation object and can be used to check the status + * // of firewall rule creation. + * }; + * * var network = compute.network('network-name'); * * var firewall = network.createFirewall('tcp-3000', diff --git a/lib/compute/operation.js b/lib/compute/operation.js new file mode 100644 index 00000000000..c863ae612c9 --- /dev/null +++ b/lib/compute/operation.js @@ -0,0 +1,143 @@ +/*! + * Copyright 2014 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/*! + * @module compute/operation + */ + +'use strict'; + +/** + * @type {module:common/util} + * @private + */ +var util = require('../common/util.js'); + +/** + * Create an Operation object to interact with a Google Compute Engine + * operation. + * + * @constructor + * + * @throws {Error} if an operation name is not provided. + * + * @param {module:compute} scope - The scope of the operation, can be either a + * `Compute` object, a `Zone` object or a `Region` object. + * @param {string} name - Operation name. + * @param {object=} metadata - Operation metadata. + * + * @example + * var gcloud = require('gcloud')({ + * keyFilename: '/path/to/keyfile.json' + * }); + * + * var compute = gcloud.compute(); + * var myZone = compute.zone('example-zone'); + * var myRegion = compute.region('example-region'); + * + * var globalOperation = compute.operation('global-operation'); + * var regionOperation = myRegion.operation('region-operation'); + * var zoneOperation = myZone.operation('zone-operation'); + */ +function Operation(scope, name, metadata) { + this.scope = scope; + this.name = name; + this.metadata = metadata; + + if (!util.is(this.name, 'string')) { + throw new Error('A name is needed to use a Compute Engine Operation.'); + } +} + +/** + * Delete the operation. + * + * @param {function} callback - The callback function. + * + * @example + * operation.delete(function(err) {}); + */ +Operation.prototype.delete = function(callback) { + callback = callback || util.noop; + this.makeReq_('DELETE', '', null, true, callback); +}; + +/** + * Get the operation's metadata. For a detailed description of metadata see + * [Operation resource](https://goo.gl/sWm1rt). + * + * @param {function=} callback - The callback function. + * + * @example + * operation.getMetadata(function(err, metadata, apiResponse) {}); + * + * //- + * // To check the status of an operation and its progress `getMetadata` in + * // combination with `setTimeout` can be used. + * //- + * var checkStatus = function() { + * operation.getMetadata(function(err, metadata, apiResponse) { + * if (err) { + * // An error occurred + * } else { + * if (metadata.status === 'DONE') { + * // the operation completed + * // `metadata.error` is set with errors if the operation failed + * // `metadata.warnings` possibly contains warnings + * } else { + * // `metadata.progress` is a progress indicator in [0,100] + * setTimeout(checkStatus, 10); + * } + * } + * }); + * }; + * + * setTimeout(checkStatus, 10); + */ +Operation.prototype.getMetadata = function(callback) { + callback = callback || util.noop; + var self = this; + this.makeReq_('GET', '', null, null, function(err, resp) { + if (err && (!resp || resp.name !== self.name)) { + callback(err); + return; + } + self.metadata = resp; + callback(null, self.metadata, resp); + }); +}; + +/** + * Make a new request object from the provided arguments and wrap the callback + * to intercept non-successful responses. + * + * @private + * + * @param {string} method - Action. + * @param {string} path - Request path. + * @param {*} query - Request query object. + * @param {*} body - Request body contents. + * @param {function} callback - The callback function. + */ +Operation.prototype.makeReq_ = function(method, path, query, body, callback) { + path = '/operations/' + this.name + path; + if (this.scope.constructor && this.scope.constructor.name === 'Compute') { + path = '/global' + path; + } + this.scope.makeReq_(method, path, query, body, callback); +}; + +module.exports = Operation; \ No newline at end of file diff --git a/lib/compute/region.js b/lib/compute/region.js index 54a298d957b..a972d4eca9a 100644 --- a/lib/compute/region.js +++ b/lib/compute/region.js @@ -28,6 +28,12 @@ var extend = require('extend'); */ var Address = require('./address.js'); +/** + * @type {module:compute/operation} + * @private + */ +var Operation = require('./operation.js'); + /** * @type {module:common/util} * @private @@ -91,6 +97,27 @@ Region.prototype.address = function(name) { return new Address(this, name); }; +/** + * Get a reference to a Google Compute Engine region operation. + * + * @param {string} name - Name of the existing operation. + * @return {module:compute/operation} + * + * @example + * var gcloud = require('gcloud')({ + * keyFilename: '/path/to/keyfile.json' + * }); + * + * var compute = gcloud.compute(); + * + * var myRegion = compute.region('region-name'); + * + * var operation = myRegion.operation('operation-name'); + */ +Region.prototype.operation = function(name) { + return new Operation(this, name); +}; + /** * Create an address in this region. For a detailed description of method's * options see [API reference](https://goo.gl/lY8Y3u). @@ -101,8 +128,10 @@ Region.prototype.address = function(name) { * @param {function} callback - The callback function. * * @example - * var callback = function(err, address, apiResponse) { + * var callback = function(err, address, operation) { * // `address` is a Address object. + * // `operation` is an Operation object and can be used to check the status + * // of address creation. * }; * * var compute = gcloud.compute({ @@ -134,7 +163,9 @@ Region.prototype.createAddress = function(name, callback) { return; } var address = self.address(name); - callback(null, address, resp); + var operation = self.operation(resp.name); + operation.metadata = resp; + callback(null, address, operation); }); }; @@ -234,6 +265,106 @@ Region.prototype.getAddresses = function(options, callback) { }); }; +/** + * Get a list of operations for this region. For a detailed description of + * method's options see [API reference](https://goo.gl/saRUxf). + * + * @param {object} options - Operation search options. + * @param {boolean=} options.autoPaginate - Have pagination handled + * automatically. Default: true. + * @param {number=} options.maxResults - Maximum number of operations to return. + * @param {string=} options.filter - Search filter. The filter must contain: + * `FIELD_NAME COMPARISON_STRING LITERAL_STRING`. `FIELD_NAME` is the name + * of the field to compare. `COMPARISON` is the comparison operator that can + * be either `eq` or `ne`. `LITERAL_STRING` is the string to filter to. + * For string fields `LITERAL_STRING` can be a regular expression. + * @param {string=} pageToken - Page identifier used in paginated search. + * @param {function} callback - The callback function. + * + * @example + * var callback = function(err, operations) { + * // `operations` is an array of `Operation` objects. + * }; + * + * var myRegion = compute.region('region-name'); + * + * myRegion.getOperations( + * { + * filter: 'name eq operation-[0-9]' + * }, callback); + * + * //- + * // To control how many API requests are made and page through the results + * // manually, set `autoPaginate` to `false`. + * //- + * var callback = function(err, operations, nextQuery, apiResponse) { + * if (nextQuery) { + * // More results exist. + * myRegion.getOperations(nextQuery, callback); + * } + * }; + * + * myRegion.getOperations({ + * autoPaginate: false + * }, callback); + * + * //- + * // Get the operations from your project as a readable object stream. + * //- + * myRegion.getOperations() + * .on('error', console.error) + * .on('data', function(operation) { + * // `operation` is a `Operation` object. + * }) + * .on('end', function() { + * // All operations retrieved. + * }); + * + * //- + * // If you anticipate many results, you can end a stream early to prevent + * // unnecessary processing and API requests. + * //- + * myRegion.getOperations() + * .on('data', function(operation) { + * this.end(); + * }); + */ +Region.prototype.getOperations = function(options, callback) { + if (util.is(options, 'function')) { + callback = options; + options = {}; + } + + options = options || {}; + + var self = this; + this.makeReq_( + 'GET', + '/operations', + options, + null, function(err, resp) { + if (err) { + callback(err); + return; + } + + var nextQuery = null; + + if (resp.nextPageToken) { + nextQuery = extend({}, options, { + pageToken: resp.nextPageToken + }); + } + + var operations = (resp.items || []).map(function(operationObject) { + var operation = self.operation(operationObject.name); + operation.metadata = operationObject; + return operation; + }); + callback(null, operations, nextQuery, resp); + }); +}; + /** * Make a new request object from the provided arguments and wrap the callback * to intercept non-successful responses. @@ -256,6 +387,6 @@ Region.prototype.makeReq_ = function(method, path, query, body, callback) { * These methods can be used with either a callback or as a readable object * stream. `streamRouter` is used to add this dual behavior. */ -streamRouter.extend(Region, ['getAddresses']); +streamRouter.extend(Region, ['getAddresses', 'getOperations']); module.exports = Region; diff --git a/lib/compute/snapshot.js b/lib/compute/snapshot.js index 78191bff5f8..4795b0f7743 100644 --- a/lib/compute/snapshot.js +++ b/lib/compute/snapshot.js @@ -64,11 +64,23 @@ function Snapshot(compute, name, metadata) { * @param {function} callback - The callback function. * * @example - * snapshot.delete(function(err) {}); + * snapshot.delete(function(err, operation) { + * // `operation` is an Operation object and can be used to check the status + * // of snapshot deletion. + * }); */ Snapshot.prototype.delete = function(callback) { callback = callback || util.noop; - this.makeReq_('DELETE', '', null, true, callback); + var compute = this.compute; + this.makeReq_('DELETE', '', null, true, function(err, resp) { + if (err) { + callback(err); + return; + } + var operation = compute.operation(resp.name); + operation.metadata = resp; + callback(null, operation); + }); }; /** diff --git a/lib/compute/zone.js b/lib/compute/zone.js index 772337aee0b..dce273810d3 100644 --- a/lib/compute/zone.js +++ b/lib/compute/zone.js @@ -34,6 +34,12 @@ var Instance = require('./instance.js'); */ var Disk = require('./disk.js'); +/** + * @type {module:compute/operation} + * @private + */ +var Operation = require('./operation.js'); + /** * @type {module:common/util} * @private @@ -118,6 +124,27 @@ Zone.prototype.disk = function(name) { return new Disk(this, name); }; +/** + * Get a reference to a Google Compute Engine zone operation. + * + * @param {string} name - Name of the existing operation. + * @return {module:compute/operation} + * + * @example + * var gcloud = require('gcloud')({ + * keyFilename: '/path/to/keyfile.json' + * }); + * + * var compute = gcloud.compute(); + * + * var myZone = compute.zone('zone-name'); + * + * var operation = myZone.operation('operation-name'); + */ +Zone.prototype.operation = function(name) { + return new Operation(this, name); +}; + /** * Create an instance in the zone. For a detailed description of method's * options see [API reference](https://goo.gl/oWcGvQ). @@ -150,8 +177,10 @@ Zone.prototype.disk = function(name) { * @param {function} callback - The callback function. * * @example - * var callback = function(err, instance, apiResponse) { + * var callback = function(err, instance, operation) { * // `instance` is an Instance object. + * // `operation` is an Operation object and can be used to check the status + * // of instance creation. * }; * * var compute = gcloud.compute({ @@ -269,7 +298,9 @@ Zone.prototype.createInstance = function(name, options, callback) { } var instance = self.instance(name); instance.metadata = options; - callback(null, instance, resp); + var operation = self.operation(resp.name); + operation.metadata = resp; + callback(null, instance, operation); }); }; @@ -386,6 +417,8 @@ Zone.prototype.getInstances = function(options, callback) { * @example * var callback = function(err, disk, apiResponse) { * // `disk` is a Disk object. + * // `operation` is an Operation object and can be used to check the status + * // of disk creation. * }; * * var compute = gcloud.compute({ @@ -442,7 +475,9 @@ Zone.prototype.createDisk = function(name, options, callback) { } var disk = self.disk(name); self.metadata = options; - callback(null, disk, resp); + var operation = self.operation(resp.name); + operation.metadata = resp; + callback(null, disk, operation); }); }; @@ -542,6 +577,106 @@ Zone.prototype.getDisks = function(options, callback) { }); }; +/** + * Get a list of operations for this zone. For a detailed description of + * method's options see [API reference](https://goo.gl/5n74cP). + * + * @param {object} options - Operation search options. + * @param {boolean=} options.autoPaginate - Have pagination handled + * automatically. Default: true. + * @param {number=} options.maxResults - Maximum number of operations to return. + * @param {string=} options.filter - Search filter. The filter must contain: + * `FIELD_NAME COMPARISON_STRING LITERAL_STRING`. `FIELD_NAME` is the name + * of the field to compare. `COMPARISON` is the comparison operator that can + * be either `eq` or `ne`. `LITERAL_STRING` is the string to filter to. + * For string fields `LITERAL_STRING` can be a regular expression. + * @param {string=} pageToken - Page identifier used in paginated search. + * @param {function} callback - The callback function. + * + * @example + * var callback = function(err, operations) { + * // `operations` is an array of `Operation` objects. + * }; + * + * var myZone = compute.zone('zone-name'); + * + * myZone.getOperations( + * { + * filter: 'name eq operation-[0-9]' + * }, callback); + * + * //- + * // To control how many API requests are made and page through the results + * // manually, set `autoPaginate` to `false`. + * //- + * var callback = function(err, operations, nextQuery, apiResponse) { + * if (nextQuery) { + * // More results exist. + * myZone.getOperations(nextQuery, callback); + * } + * }; + * + * myZone.getOperations({ + * autoPaginate: false + * }, callback); + * + * //- + * // Get the operations from your project as a readable object stream. + * //- + * myZone.getOperations() + * .on('error', console.error) + * .on('data', function(operation) { + * // `operation` is a `Operation` object. + * }) + * .on('end', function() { + * // All operations retrieved. + * }); + * + * //- + * // If you anticipate many results, you can end a stream early to prevent + * // unnecessary processing and API requests. + * //- + * myZone.getOperations() + * .on('data', function(operation) { + * this.end(); + * }); + */ +Zone.prototype.getOperations = function(options, callback) { + if (util.is(options, 'function')) { + callback = options; + options = {}; + } + + options = options || {}; + + var self = this; + this.makeReq_( + 'GET', + '/operations', + options, + null, function(err, resp) { + if (err) { + callback(err); + return; + } + + var nextQuery = null; + + if (resp.nextPageToken) { + nextQuery = extend({}, options, { + pageToken: resp.nextPageToken + }); + } + + var operations = (resp.items || []).map(function(operationObject) { + var operation = self.operation(operationObject.name); + operation.metadata = operationObject; + return operation; + }); + callback(null, operations, nextQuery, resp); + }); +}; + /** * Make a new request object from the provided arguments and wrap the callback * to intercept non-successful responses. @@ -564,6 +699,6 @@ Zone.prototype.makeReq_ = function(method, path, query, body, callback) { * These methods can be used with either a callback or as a readable object * stream. `streamRouter` is used to add this dual behavior. */ -streamRouter.extend(Zone, ['getInstances', 'getDisks']); +streamRouter.extend(Zone, ['getInstances', 'getDisks', 'getOperations']); module.exports = Zone; \ No newline at end of file