diff --git a/README.md b/README.md
index 2f4f22b149d..1e056347fa9 100644
--- a/README.md
+++ b/README.md
@@ -15,6 +15,7 @@ This client supports the following Google Cloud Platform services:
* [Google Cloud DNS](#google-cloud-dns)
* [Google Cloud Pub/Sub](#google-cloud-pubsub)
* [Google Cloud Storage](#google-cloud-storage)
+* [Google Compute Engine](#google-compute-engine)
* [Google Cloud Search](#google-cloud-search-alpha) (Alpha)
If you need support for other Google APIs, check out the [Google Node.js API Client library][googleapis].
@@ -300,6 +301,40 @@ localReadStream.pipe(remoteWriteStream);
```
+## Google Compute Engine
+
+- [API Documentation][gcloud-compute-docs]
+- [Official Documentation][cloud-compute-docs]
+
+#### Preview
+
+```js
+var gcloud = require('gcloud');
+
+// Authorizing on a per-API-basis. You don't need to do this if you auth on a
+// global basis (see Authorization section above).
+
+var gce = gcloud.compute({
+ projectId: 'my-project',
+ keyFilename: '/path/to/keyfile.json'
+});
+
+// Create a new VM using the latest OS image of your choice.
+var zone = gce.zone('us-central1-a');
+var name = 'ubuntu-http';
+
+zone.createVM(name, { os: 'ubuntu' }, function(err, vm, operation) {
+ // `operation` lets you check the status of long-running tasks.
+
+ operation.onComplete(function(err, metadata) {
+ if (!err) {
+ // Virtual machine created!
+ }
+ });
+});
+```
+
+
## Google Cloud Search (Alpha)
> This is an *Alpha* release of Google Cloud Search. This feature is not covered by any SLA or deprecation policy and may be subject to backward-incompatible changes.
@@ -357,6 +392,7 @@ Apache 2.0 - See [COPYING](COPYING) for more information.
[gcloud-homepage]: https://googlecloudplatform.github.io/gcloud-node/
[gcloud-docs]: https://googlecloudplatform.github.io/gcloud-node/#/docs
[gcloud-bigquery-docs]: https://googlecloudplatform.github.io/gcloud-node/#/docs/bigquery
+[gcloud-compute-docs]: https://googlecloudplatform.github.io/gcloud-node/#/docs/compute
[gcloud-datastore-docs]: https://googlecloudplatform.github.io/gcloud-node/#/docs/datastore
[gcloud-dns-docs]: https://googlecloudplatform.github.io/gcloud-node/#/docs/dns
[gcloud-pubsub-docs]: https://googlecloudplatform.github.io/gcloud-node/#/docs/pubsub
@@ -376,6 +412,8 @@ Apache 2.0 - See [COPYING](COPYING) for more information.
[cloud-bigquery-docs]: https://cloud.google.com/bigquery/what-is-bigquery
+[cloud-compute-docs]: https://cloud.google.com/compute/docs
+
[cloud-datastore-docs]: https://cloud.google.com/datastore/docs
[cloud-datastore-activation]: https://cloud.google.com/datastore/docs/activate
diff --git a/docs/json/master/compute/.gitkeep b/docs/json/master/compute/.gitkeep
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/docs/site/components/docs/compute-overview.html b/docs/site/components/docs/compute-overview.html
new file mode 100644
index 00000000000..b0198ac00d9
--- /dev/null
+++ b/docs/site/components/docs/compute-overview.html
@@ -0,0 +1,7 @@
+
Compute Engine Overview
+
+ The object returned from gcloud.compute gives you complete control of your Compute Engine virtual machines, disks, networks, snapshots, addresses, firewalls, and more.
+
+
+ To learn more about Compute Engine, see What is Google Compute Engine?
+
diff --git a/docs/site/components/docs/docs-values.js b/docs/site/components/docs/docs-values.js
index 3bd5b3d4e39..59c2ef1638f 100644
--- a/docs/site/components/docs/docs-values.js
+++ b/docs/site/components/docs/docs-values.js
@@ -39,6 +39,49 @@ angular.module('gcloud.docs')
]
},
+ compute: {
+ title: 'Compute',
+ _url: '{baseUrl}/compute',
+ pages: [
+ {
+ title: 'Address',
+ url: '/address'
+ },
+ {
+ title: 'Disk',
+ url: '/disk'
+ },
+ {
+ title: 'Firewall',
+ url: '/firewall'
+ },
+ {
+ title: 'Network',
+ url: '/network'
+ },
+ {
+ title: 'Operation',
+ url: '/operation'
+ },
+ {
+ title: 'Region',
+ url: '/region'
+ },
+ {
+ title: 'Snapshot',
+ url: '/snapshot'
+ },
+ {
+ title: 'VM',
+ url: '/vm'
+ },
+ {
+ title: 'Zone',
+ url: '/zone'
+ }
+ ]
+ },
+
datastore: {
title: 'Datastore',
_url: '{baseUrl}/datastore',
@@ -180,6 +223,9 @@ angular.module('gcloud.docs')
'>=0.16.0': ['search'],
// introduce dns api.
- '>=0.18.0': ['dns']
+ '>=0.18.0': ['dns'],
+
+ // introduce compute api.
+ '>=0.20.0': ['compute']
}
});
diff --git a/docs/site/components/docs/docs.html b/docs/site/components/docs/docs.html
index 3b4450ee0d3..1d94fa00dbd 100644
--- a/docs/site/components/docs/docs.html
+++ b/docs/site/components/docs/docs.html
@@ -44,7 +44,7 @@
-
diff --git a/lib/compute/address.js b/lib/compute/address.js
new file mode 100644
index 00000000000..45e3b3e8771
--- /dev/null
+++ b/lib/compute/address.js
@@ -0,0 +1,145 @@
+/*!
+ * Copyright 2015 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');
+
+/*! Developer Documentation
+ *
+ * @param {module:region} region - Region this address belongs to.
+ * @param {string} name - The name of the address.
+ */
+/**
+ * An Address object allows you to interact with a Google Compute Engine
+ * address.
+ *
+ * @resource [Instances and Networks]{@link https://cloud.google.com/compute/docs/instances-and-network}
+ * @resource [Address Resource]{@link https://cloud.google.com/compute/docs/reference/v1/addresses} *
+ *
+ * @constructor
+ * @alias module:compute/address
+ *
+ * @example
+ * var gcloud = require('gcloud')({
+ * keyFilename: '/path/to/keyfile.json',
+ * projectId: 'grape-spaceship-123'
+ * });
+ *
+ * var gce = gcloud.compute();
+ *
+ * var region = gce.region('region-name');
+ *
+ * var address = region.address('address1');
+ */
+function Address(region, name) {
+ this.region = region;
+ this.name = name;
+ this.metadata = {};
+}
+
+/**
+ * Delete the address.
+ *
+ * @resource [Addresses: delete API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/addresses/delete}
+ *
+ * @param {function=} callback - The callback function.
+ * @param {?error} callback.err - An error returned while making this request.
+ * @param {module:compute/operation} callback.operation - An operation object
+ * that can be used to check the status of the request.
+ * @param {object} callback.apiResponse - The full API response.
+ *
+ * @example
+ * address.delete(function(err, operation, apiResponse) {
+ * // `operation` is an Operation object that can be used to check the status
+ * // of the request.
+ * });
+ */
+Address.prototype.delete = function(callback) {
+ callback = callback || util.noop;
+
+ var region = this.region;
+
+ this.makeReq_('DELETE', '', null, null, function(err, resp) {
+ if (err) {
+ callback(err, null, resp);
+ return;
+ }
+
+ var operation = region.operation(resp.name);
+ operation.metadata = resp;
+
+ callback(null, operation, resp);
+ });
+};
+
+/**
+ * Get the metadata of this address.
+ *
+ * @resource [Address Resource]{@link https://cloud.google.com/compute/docs/reference/v1/addresses}
+ * @resource [Addresses: get API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/addresses/get}
+ *
+ * @param {function=} callback - The callback function.
+ * @param {?error} callback.err - An error returned while making this request
+ * @param {object} callback.metadata - The address's metadata.
+ * @param {object} callback.apiResponse - The full API response.
+ *
+ * @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, null, resp);
+ 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;
diff --git a/lib/compute/disk.js b/lib/compute/disk.js
new file mode 100644
index 00000000000..87e1cf0684e
--- /dev/null
+++ b/lib/compute/disk.js
@@ -0,0 +1,220 @@
+/*!
+ * Copyright 2015 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';
+
+var extend = require('extend');
+var format = require('string-format-obj');
+var is = require('is');
+
+/**
+ * @type {module:common/util}
+ * @private
+ */
+var util = require('../common/util.js');
+
+/*! Developer Documentation
+ *
+ * @param {module:zone} zone - Zone this disk belongs to.
+ * @param {string} name - The name of the disk.
+ */
+/**
+ * A Disk object allows you to interact with a Google Compute Engine disk.
+ *
+ * @resource [Disks Overview]{@link https://cloud.google.com/compute/docs/disks}
+ * @resource [Disk Resource]{@link https://cloud.google.com/compute/docs/reference/v1/disks}
+ *
+ * @constructor
+ * @alias module:compute/disk
+ *
+ * @example
+ * var gcloud = require('gcloud')({
+ * keyFilename: '/path/to/keyfile.json',
+ * projectId: 'grape-spaceship-123'
+ * });
+ *
+ * var gce = gcloud.compute();
+ *
+ * var zone = gce.zone('zone-name');
+ *
+ * var disk = zone.disk('disk1');
+ */
+function Disk(zone, name) {
+ this.zone = zone;
+ this.name = name;
+ this.metadata = {};
+
+ this.formattedName = Disk.formatName_(zone, name);
+}
+
+/**
+ * Format a disk's name how the API expects.
+ *
+ * @param {module:compute/zone} zone - The Zone this disk belongs to.
+ * @param {string} name - The name of the disk.
+ * @return {string} - The formatted name.
+ */
+Disk.formatName_ = function(zone, name) {
+ return format('projects/{pId}/zones/{zoneName}/disks/{diskName}', {
+ pId: zone.compute.projectId,
+ zoneName: zone.name,
+ diskName: name
+ });
+};
+
+/**
+ * Create a snapshot of a disk.
+ *
+ * @resource [Snapshots Overview]{@link https://cloud.google.com/compute/docs/disks/persistent-disks#snapshots}
+ * @resource [Disks: createSnapshot API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/disks/createSnapshot}
+ *
+ * @param {string} name - Name of the snapshot.
+ * @param {object=} options - See the
+ * [Disks: createSnapshot](https://cloud.google.com/compute/docs/reference/v1/disks/createSnapshot)
+ * request body.
+ * @param {function} callback - The callback function.
+ * @param {?error} callback.err - An error returned while making this request.
+ * @param {module:compute/snapshot} callback.snapshot - The created Snapshot
+ * object.
+ * @param {module:compute/operation} callback.operation - An operation object
+ * that can be used to check the status of the request.
+ * @param {object} callback.apiResponse - The full API response.
+ *
+ * @example
+ * function callback(err, snapshot, operation, apiResponse) {
+ * // `snapshot` is a Snapshot object.
+ *
+ * // `operation` is an Operation object that can be used to check the status
+ * // of the request.
+ * }
+ *
+ * disk.createSnapshot('new-snapshot-name', callback);
+ */
+Disk.prototype.createSnapshot = function(name, options, callback) {
+ var zone = this.zone;
+
+ if (is.fn(options)) {
+ callback = options;
+ options = {};
+ }
+
+ var body = extend({}, options, {
+ name: name
+ });
+
+ this.makeReq_('POST', '/createSnapshot', null, body, function(err, resp) {
+ if (err) {
+ callback(err, null, null, resp);
+ return;
+ }
+
+ var snapshot = zone.compute.snapshot(name);
+
+ var operation = zone.operation(resp.name);
+ operation.metadata = resp;
+
+ callback(null, snapshot, operation, resp);
+ });
+};
+
+/**
+ * Delete the disk.
+ *
+ * @resource [Disks: delete API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/disks/delete}
+ *
+ * @param {function=} callback - The callback function.
+ * @param {?error} callback.err - An error returned while making this request.
+ * @param {module:compute/operation} callback.operation - An operation object
+ * that can be used to check the status of the request.
+ * @param {object} callback.apiResponse - The full API response.
+ *
+ * @example
+ * disk.delete(function(err, operation, apiResponse) {
+ * // `operation` is an Operation object that can be used to check the status
+ * // of the request.
+ * });
+ */
+Disk.prototype.delete = function(callback) {
+ var zone = this.zone;
+
+ callback = callback || util.noop;
+
+ this.makeReq_('DELETE', '', null, null, function(err, resp) {
+ if (err) {
+ callback(err, null, resp);
+ return;
+ }
+
+ var operation = zone.operation(resp.name);
+ operation.metadata = resp;
+
+ callback(null, operation, resp);
+ });
+};
+
+/**
+ * Get the disk's metadata.
+ *
+ * @resource [Disk Resource]{@link https://cloud.google.com/compute/docs/reference/v1/disks}
+ * @resource [Disks: get API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/disks/get}
+ *
+ * @param {function=} callback - The callback function.
+ * @param {?error} callback.err - An error returned while making this request
+ * @param {object} callback.metadata - The disk's metadata.
+ * @param {object} callback.apiResponse - The full API response.
+ *
+ * @example
+ * disk.getMetadata(function(err, metadata, apiResponse) {});
+ */
+Disk.prototype.getMetadata = function(callback) {
+ var self = this;
+
+ callback = callback || util.noop;
+
+ this.makeReq_('GET', '', null, null, function(err, resp) {
+ if (err) {
+ callback(err, null, resp);
+ 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.
+ */
+Disk.prototype.makeReq_ = function(method, path, query, body, callback) {
+ path = '/disks/' + this.name + path;
+ this.zone.makeReq_(method, path, query, body, callback);
+};
+
+module.exports = Disk;
diff --git a/lib/compute/firewall.js b/lib/compute/firewall.js
new file mode 100644
index 00000000000..fc734fa52bc
--- /dev/null
+++ b/lib/compute/firewall.js
@@ -0,0 +1,191 @@
+/*!
+ * Copyright 2015 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');
+
+/*! Developer Documentation
+ *
+ * @param {module:compute} compute - Compute object this firewall belongs to.
+ * @param {string} name - Name of the firewall.
+ */
+/**
+ * A Firewall object allows you to interact with a Google Compute Engine
+ * firewall.
+ *
+ * @resource [Firewalls Overview]{@link https://cloud.google.com/compute/docs/networking#firewalls}
+ * @resource [Firewall Resource]{@link https://cloud.google.com/compute/docs/reference/v1/firewalls}
+ *
+ * @constructor
+ * @alias module:compute/firewall
+ *
+ * @example
+ * var gcloud = require('gcloud')({
+ * keyFilename: '/path/to/keyfile.json',
+ * projectId: 'grape-spaceship-123'
+ * });
+ *
+ * var gce = gcloud.compute();
+ *
+ * var firewall = gce.firewall('tcp-3000');
+ */
+function Firewall(compute, name) {
+ this.compute = compute;
+ this.name = name;
+
+ this.metadata = {
+ network: 'global/networks/default'
+ };
+}
+
+/**
+ * Delete the firewall.
+ *
+ * @resource [Firewalls: delete API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/firewalls/delete}
+ *
+ * @param {function=} callback - The callback function.
+ * @param {?error} callback.err - An error returned while making this request.
+ * @param {module:compute/operation} callback.operation - An operation object
+ * that can be used to check the status of the request.
+ * @param {object} callback.apiResponse - The full API response.
+ *
+ * @example
+ * firewall.delete(function(err, operation, apiResponse) {
+ * // `operation` is an Operation object that can be used to check the status
+ * // of the request.
+ * });
+ */
+Firewall.prototype.delete = function(callback) {
+ var compute = this.compute;
+
+ callback = callback || util.noop;
+
+ this.makeReq_('DELETE', '', null, null, function(err, resp) {
+ if (err) {
+ callback(err, null, resp);
+ return;
+ }
+
+ var operation = compute.operation(resp.name);
+ operation.metadata = resp;
+
+ callback(null, operation, resp);
+ });
+};
+
+/**
+ * Get the firewall's metadata.
+ *
+ * @resource [Firewalls: get API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/firewalls/get}
+ * @resource [Firewall Resource]{@link https://cloud.google.com/compute/docs/reference/v1/firewalls}
+ *
+ * @param {function=} callback - The callback function.
+ * @param {?error} callback.err - An error returned while making this request
+ * @param {object} callback.metadata - The network's metadata.
+ * @param {object} callback.apiResponse - The full API response.
+ *
+ * @example
+ * firewall.getMetadata(function(err, metadata, apiResponse) {});
+ */
+Firewall.prototype.getMetadata = function(callback) {
+ var self = this;
+
+ callback = callback || util.noop;
+
+ this.makeReq_('GET', '', null, null, function(err, resp) {
+ if (err) {
+ callback(err, null, resp);
+ return;
+ }
+
+ self.metadata = resp;
+
+ callback(null, self.metadata, resp);
+ });
+};
+
+/**
+ * Set the firewall's metadata.
+ *
+ * @resource [Firewall Resource]{@link https://cloud.google.com/compute/docs/reference/v1/firewalls}
+ *
+ * @param {object} metadata - See a
+ * [Firewall resource](https://cloud.google.com/compute/docs/reference/v1/firewalls).
+ * @param {function=} callback - The callback function.
+ * @param {?error} callback.err - An error returned while making this request.
+ * @param {module:compute/operation} callback.operation - An operation object
+ * that can be used to check the status of the request.
+ * @param {object} callback.apiResponse - The full API response.
+ *
+ * @example
+ * var metadata = {
+ * description: 'New description'
+ * };
+ *
+ * firewall.setMetadata(metadata, function(err, operation, apiResponse) {
+ * // `operation` is an Operation object that can be used to check the status
+ * // of the request.
+ * });
+ */
+Firewall.prototype.setMetadata = function(metadata, callback) {
+ var compute = this.compute;
+
+ callback = callback || util.noop;
+
+ metadata = metadata || {};
+ metadata.name = this.name;
+ metadata.network = this.metadata.network;
+
+ this.makeReq_('PATCH', '', null, metadata, function(err, resp) {
+ if (err) {
+ callback(err, null, resp);
+ return;
+ }
+
+ var operation = compute.operation(resp.name);
+ operation.metadata = resp;
+
+ callback(null, operation, 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.
+ */
+Firewall.prototype.makeReq_ = function(method, path, query, body, callback) {
+ path = '/global/firewalls/' + this.name + path;
+ this.compute.makeReq_(method, path, query, body, callback);
+};
+
+module.exports = Firewall;
diff --git a/lib/compute/index.js b/lib/compute/index.js
new file mode 100644
index 00000000000..eed2cb9bc44
--- /dev/null
+++ b/lib/compute/index.js
@@ -0,0 +1,1391 @@
+/*!
+ * Copyright 2015 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';
+
+var arrify = require('arrify');
+var extend = require('extend');
+var is = require('is');
+
+/**
+ * @type {module:compute/firewall}
+ * @private
+ */
+var Firewall = require('./firewall.js');
+
+/**
+ * @type {module:compute/network}
+ * @private
+ */
+var Network = require('./network.js');
+
+/**
+ * @type {module:compute/operation}
+ * @private
+ */
+var Operation = require('./operation.js');
+
+/**
+ * @type {module:compute/region}
+ * @private
+ */
+var Region = require('./region.js');
+
+/**
+ * @type {module:compute/snapshot}
+ * @private
+ */
+var Snapshot = require('./snapshot.js');
+
+/**
+ * @type {module:common/streamrouter}
+ * @private
+ */
+var streamRouter = require('../common/stream-router.js');
+
+/**
+ * @type {module:common/util}
+ * @private
+ */
+var util = require('../common/util.js');
+
+/**
+ * @type {module:compute/zone}
+ * @private
+ */
+var Zone = require('./zone.js');
+
+/**
+ * @const {string}
+ * @private
+ */
+var COMPUTE_BASE_URL = 'https://www.googleapis.com/compute/v1/projects/';
+
+/**
+ * Required scopes for Google Compute Engine API.
+ * @const {array}
+ * @private
+ */
+var SCOPES = ['https://www.googleapis.com/auth/compute'];
+
+/**
+ * A Compute object allows you to interact with the Google Compute Engine API.
+ * Using this object, you can access your instances with {module:compute/vm},
+ * disks with {module:compute/disk}, and firewalls with
+ * {module:compute/firewall}.
+ *
+ * @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 gce = gcloud.compute();
+ */
+function Compute(options) {
+ if (!(this instanceof Compute)) {
+ return new Compute(options);
+ }
+
+ options = options || {};
+
+ if (!options.projectId) {
+ throw util.missingProjectIdError;
+ }
+
+ var authConfig = {
+ credentials: options.credentials,
+ keyFile: options.keyFilename,
+ scopes: SCOPES,
+ email: options.email
+ };
+
+ // We store the authConfig for use with gceImages in Zone.
+ this.authConfig = authConfig;
+
+ this.makeAuthorizedRequest_ = util.makeAuthorizedRequestFactory(authConfig);
+ this.projectId = options.projectId;
+}
+
+/**
+ * Create a firewall.
+ *
+ * @resource [Firewalls Overview]{@link https://cloud.google.com/compute/docs/networking#firewalls}
+ * @resource [Firewalls: insert API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/firewalls/insert}
+ *
+ * @throws {Error} if a name is not provided.
+ * @throws {Error} if a config object is not provided.
+ *
+ * @param {string} name - Name of the firewall.
+ * @param {object} config - See a
+ * [Firewall resource](https://cloud.google.com/compute/docs/reference/v1/firewalls#resource).
+ * @param {object} config.protocols - A map of protocol to port range. The keys
+ * of the object refer to a protocol (e.g. `tcp`, `udp`) and the value for
+ * the key are the ports/port-ranges that are allowed to make a connection.
+ * @param {string[]} config.ranges - The IP address blocks that this rule
+ * applies to, expressed in
+ * [CIDR](http://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing)
+ * format.
+ * @param {string[]} config.tags - Instance tags which this rule applies to.
+ * @param {function} callback - The callback function.
+ * @param {?error} callback.err - An error returned while making this request.
+ * @param {module:compute/firewall} callback.firewall - The created Firewall
+ * object.
+ * @param {module:compute/operation} callback.operation - An operation object
+ * that can be used to check the status of the request.
+ * @param {object} callback.apiResponse - The full API response.
+ *
+ * @example
+ * var config = {
+ * protocols: {
+ * tcp: [3000],
+ * udp: [] // An empty array means all ports are allowed.
+ * },
+ *
+ * ranges: ['0.0.0.0/0']
+ * };
+ *
+ * function callback(err, firewall, operation, apiResponse) {
+ * // `firewall` is a Firewall object.
+ *
+ * // `operation` is an Operation object that can be used to check the status
+ * // of the request.
+ * }
+ *
+ * gce.createFirewall('new-firewall-name', config, callback);
+ */
+Compute.prototype.createFirewall = function(name, config, callback) {
+ var self = this;
+
+ if (!is.string(name)) {
+ throw new Error('A firewall name must be provided.');
+ }
+
+ if (!is.object(config)) {
+ throw new Error('A firewall configuration object must be provided.');
+ }
+
+ var body = extend({}, config, {
+ name: name
+ });
+
+ if (body.protocols) {
+ body.allowed = arrify(body.allowed);
+
+ for (var protocol in body.protocols) {
+ var allowedConfig = {
+ IPProtocol: protocol
+ };
+
+ var ports = arrify(body.protocols[protocol]);
+ if (ports.length > 0) {
+ allowedConfig.ports = ports;
+ }
+
+ body.allowed.push(allowedConfig);
+ }
+
+ delete body.protocols;
+ }
+
+ if (body.ranges) {
+ body.sourceRanges = arrify(body.ranges);
+ delete body.ranges;
+ }
+
+ if (body.tags) {
+ body.sourceTags = arrify(body.tags);
+ delete body.tags;
+ }
+
+ var path = '/global/firewalls';
+
+ this.makeReq_('POST', path, null, body, function(err, resp) {
+ if (err) {
+ callback(err, null, null, resp);
+ return;
+ }
+
+ var firewall = self.firewall(name);
+
+ var operation = self.operation(resp.name);
+ operation.metadata = resp;
+
+ callback(null, firewall, operation, resp);
+ });
+};
+
+/**
+ * Create a network.
+ *
+ * @resource [Networks Overview]{@link https://cloud.google.com/compute/docs/networking#networks}
+ * @resource [Networks: insert API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/networks/insert}
+ *
+ * @param {string} name - Name of the network.
+ * @param {object} config - See a
+ * [Network resource](https://cloud.google.com/compute/docs/reference/v1/networks#resource).
+ * @param {string} config.gateway - A gateway address for default routing to
+ * other networks. (Alias for `config.gatewayIPv4`)
+ * @param {string} config.range -
+ * [CIDR](http://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing) range
+ * of addresses that are legal on this network. (Alias for
+ * `config.IPv4Range`)
+ * @param {function=} callback - The callback function.
+ * @param {?error} callback.err - An error returned while making this request.
+ * @param {module:compute/network} callback.network - The created Network
+ * object.
+ * @param {module:compute/operation} callback.operation - An operation object
+ * that can be used to check the status of the request.
+ * @param {object} callback.apiResponse - The full API response.
+ *
+ * @example
+ * var config = {
+ * range: '10.240.0.0/16'
+ * };
+ *
+ * function callback(err, network, operation, apiResponse) {
+ * // `network` is a Network object.
+ *
+ * // `operation` is an Operation object and can be used to check the status
+ * // of network creation.
+ * }
+ *
+ * gce.createNetwork('new-network', config, callback);
+ */
+Compute.prototype.createNetwork = function(name, config, callback) {
+ var self = this;
+
+ var body = extend({}, config, {
+ name: name
+ });
+
+ if (body.range) {
+ body.IPv4Range = body.range;
+ delete body.range;
+ }
+
+ if (body.gateway) {
+ body.gatewayIPv4 = body.gateway;
+ delete body.gateway;
+ }
+
+ this.makeReq_('POST', '/global/networks', null, body, function(err, resp) {
+ if (err) {
+ callback(err, null, null, resp);
+ return;
+ }
+
+ var network = self.network(name);
+
+ var operation = self.operation(resp.name);
+ operation.metadata = resp;
+
+ callback(null, network, operation, resp);
+ });
+};
+
+/**
+ * Get a reference to a Google Compute Engine firewall.
+ *
+ * See {module:compute/network#firewall} to get a Firewall object for a specific
+ * network.
+ *
+ * @resource [Firewalls Overview]{@link https://cloud.google.com/compute/docs/networking#firewalls}
+ *
+ * @param {string} name - Name of the existing firewall.
+ * @return {module:compute/firewall}
+ *
+ * @example
+ * var firewall = gce.firewall('existing-firewall');
+ */
+Compute.prototype.firewall = function(name) {
+ return new Firewall(this, name);
+};
+
+/**
+ * Get a list of addresses. For a detailed description of method's options see
+ * [API reference](https://goo.gl/r9XmXJ).
+ *
+ * @resource [Instances and Networks]{@link https://cloud.google.com/compute/docs/instances-and-network}
+ * @resource [Addresses: aggregatedList API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/addresses/aggregatedList}
+ *
+ * @param {object=} options - Address search options.
+ * @param {boolean} options.autoPaginate - Have pagination handled
+ * automatically. Default: true.
+ * @param {string} options.filter - Search filter in the format of
+ * `{name} {comparison} {filterString}`.
+ * - **`name`**: the name of the field to compare
+ * - **`comparison`**: the comparison operator, `eq` (equal) or `ne`
+ * (not equal)
+ * - **`filterString`**: the string to filter to. For string fields, this
+ * can be a regular expression.
+ * @param {number} options.maxResults - Maximum number of addresses to return.
+ * @param {string} options.pageToken - A previously-returned page token
+ * representing part of the larger set of results to view.
+ * @param {function} callback - The callback function.
+ * @param {?error} callback.err - An error returned while making this request.
+ * @param {module:compute/address} callback.addresses - Address objects from
+ * your project.
+ * @param {?object} callback.nextQuery - If present, query with this object to
+ * check for more results.
+ * @param {object} callback.apiResponse - The full API response.
+ *
+ * @example
+ * gce.getAddresses(function(err, addresses) {
+ * // addresses is an array of `Address` objects.
+ * });
+ *
+ * //-
+ * // To control how many API requests are made and page through the results
+ * // manually, set `autoPaginate` to `false`.
+ * //-
+ * function callback(err, addresses, nextQuery, apiResponse) {
+ * if (nextQuery) {
+ * // More results exist.
+ * gce.getAddresses(nextQuery, callback);
+ * }
+ * }
+ *
+ * gce.getAddresses({
+ * autoPaginate: false
+ * }, callback);
+ *
+ * //-
+ * // Get the addresses from your project as a readable object stream.
+ * //-
+ * gce.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.
+ * //-
+ * gce.getAddresses()
+ * .on('data', function(address) {
+ * this.end();
+ * });
+ */
+Compute.prototype.getAddresses = function(options, callback) {
+ var self = this;
+
+ if (is.fn(options)) {
+ callback = options;
+ options = {};
+ }
+
+ options = options || {};
+
+ var path = '/aggregated/addresses';
+
+ this.makeReq_('GET', path, options, null, function(err, resp) {
+ if (err) {
+ callback(err, null, null, resp);
+ return;
+ }
+
+ var nextQuery = null;
+
+ if (resp.nextPageToken) {
+ nextQuery = extend({}, options, {
+ pageToken: resp.nextPageToken
+ });
+ }
+
+ var regions = resp.items || {};
+
+ var addresses = Object.keys(regions).reduce(function(acc, regionName) {
+ var region = self.region(regionName.replace('regions/', ''));
+ var regionAddresses = regions[regionName].addresses || [];
+
+ regionAddresses.forEach(function(address) {
+ var addressInstance = region.address(address.name);
+ addressInstance.metadata = address;
+ acc.push(addressInstance);
+ });
+
+ return acc;
+ }, []);
+
+ callback(null, addresses, nextQuery, resp);
+ });
+};
+
+/**
+ * Get a list of disks.
+ *
+ * @resource [Disks Overview]{@link https://cloud.google.com/compute/docs/disks}
+ * @resource [Disks: aggregatedList API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/disks/aggregatedList}
+ *
+ * @param {object=} options - Disk search options.
+ * @param {boolean} options.autoPaginate - Have pagination handled
+ * automatically. Default: true.
+ * @param {string} options.filter - Search filter in the format of
+ * `{name} {comparison} {filterString}`.
+ * - **`name`**: the name of the field to compare
+ * - **`comparison`**: the comparison operator, `eq` (equal) or `ne`
+ * (not equal)
+ * - **`filterString`**: the string to filter to. For string fields, this
+ * can be a regular expression.
+ * @param {number} options.maxResults - Maximum number of disks to return.
+ * @param {string} options.pageToken - A previously-returned page token
+ * representing part of the larger set of results to view.
+ * @param {function} callback - The callback function.
+ * @param {?error} callback.err - An error returned while making this request.
+ * @param {module:compute/disk} callback.disks - Disk objects from your project.
+ * @param {?object} callback.nextQuery - If present, query with this object to
+ * check for more results.
+ * @param {object} callback.apiResponse - The full API response.
+ *
+ * @example
+ * gce.getDisks(function(err, disks) {
+ * // `disks` is an array of `Disk` objects.
+ * });
+ *
+ * //-
+ * // To control how many API requests are made and page through the results
+ * // manually, set `autoPaginate` to `false`.
+ * //-
+ * function callback(err, disks, nextQuery, apiResponse) {
+ * if (nextQuery) {
+ * // More results exist.
+ * gce.getDisks(nextQuery, callback);
+ * }
+ * }
+ *
+ * gce.getDisks({
+ * autoPaginate: false
+ * }, callback);
+ *
+ * //-
+ * // Get the disks from your project as a readable object stream.
+ * //-
+ * gce.getDisks()
+ * .on('error', console.error)
+ * .on('data', function(disk) {
+ * // `disk` is a `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.
+ * //-
+ * gce.getDisks()
+ * .on('data', function(disk) {
+ * this.end();
+ * });
+ */
+Compute.prototype.getDisks = function(options, callback) {
+ var self = this;
+
+ if (is.fn(options)) {
+ callback = options;
+ options = {};
+ }
+
+ options = options || {};
+
+ this.makeReq_('GET', '/aggregated/disks', options, null, function(err, resp) {
+ if (err) {
+ callback(err, null, null, resp);
+ return;
+ }
+
+ var nextQuery = null;
+
+ if (resp.nextPageToken) {
+ nextQuery = extend({}, options, {
+ pageToken: resp.nextPageToken
+ });
+ }
+
+ var zones = resp.items || {};
+
+ var disks = Object.keys(zones).reduce(function(acc, zoneName) {
+ var zone = self.zone(zoneName.replace('zones/', ''));
+ var disks = zones[zoneName].disks || [];
+
+ disks.forEach(function(disk) {
+ var diskInstance = zone.disk(disk.name);
+ diskInstance.metadata = disk;
+ acc.push(diskInstance);
+ });
+
+ return acc;
+ }, []);
+
+ callback(null, disks, nextQuery, resp);
+ });
+};
+
+/**
+ * Get a list of firewalls.
+ *
+ * @resource [Firewalls Overview]{@link https://cloud.google.com/compute/docs/networking#firewalls}
+ * @resource [Firewalls: list API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/firewalls/list}
+ *
+ * @param {object=} options - Firewall search options.
+ * @param {boolean} options.autoPaginate - Have pagination handled
+ * automatically. Default: true.
+ * @param {string} options.filter - Search filter in the format of
+ * `{name} {comparison} {filterString}`.
+ * - **`name`**: the name of the field to compare
+ * - **`comparison`**: the comparison operator, `eq` (equal) or `ne`
+ * (not equal)
+ * - **`filterString`**: the string to filter to. For string fields, this
+ * can be a regular expression.
+ * @param {number} options.maxResults - Maximum number of firewalls to return.
+ * @param {string} options.pageToken - A previously-returned page token
+ * representing part of the larger set of results to view.
+ * @param {function} callback - The callback function.
+ * @param {?error} callback.err - An error returned while making this request.
+ * @param {module:compute/firewall} callback.firewalls - Firewall objects from
+ * your project.
+ * @param {?object} callback.nextQuery - If present, query with this object to
+ * check for more results.
+ * @param {object} callback.apiResponse - The full API response.
+ *
+ * @example
+ * gce.getFirewalls(function(err, firewalls) {
+ * // `firewalls` is an array of `Firewall` objects.
+ * });
+ *
+ * //-
+ * // To control how many API requests are made and page through the results
+ * // manually, set `autoPaginate` to `false`.
+ * //-
+ * function callback(err, firewalls, nextQuery, apiResponse) {
+ * if (nextQuery) {
+ * // More results exist.
+ * gce.getFirewalls(nextQuery, callback);
+ * }
+ * }
+ *
+ * gce.getFirewalls({
+ * autoPaginate: false
+ * }, callback);
+ *
+ * //-
+ * // Get the firewalls from your project as a readable object stream.
+ * //-
+ * gce.getFirewalls()
+ * .on('error', console.error)
+ * .on('data', function(firewall) {
+ * // `firewall` is a `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.
+ * //-
+ * gce.getFirewalls()
+ * .on('data', function(firewall) {
+ * this.end();
+ * });
+ */
+Compute.prototype.getFirewalls = function(options, callback) {
+ var self = this;
+
+ if (is.fn(options)) {
+ callback = options;
+ options = {};
+ }
+
+ options = options || {};
+
+ this.makeReq_('GET', '/global/firewalls', options, null, function(err, resp) {
+ if (err) {
+ callback(err, null, null, resp);
+ return;
+ }
+
+ var nextQuery = null;
+
+ if (resp.nextPageToken) {
+ nextQuery = extend({}, options, {
+ pageToken: resp.nextPageToken
+ });
+ }
+
+ var firewalls = (resp.items || []).map(function(firewall) {
+ var firewallInstance = self.firewall(firewall.name);
+ firewallInstance.metadata = firewall;
+ return firewallInstance;
+ });
+
+ callback(null, firewalls, nextQuery, resp);
+ });
+};
+
+/**
+ * Get a list of networks.
+ *
+ * @resource [Networks Overview]{@link https://cloud.google.com/compute/docs/networking#networks}
+ * @resource [Networks: list API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/networks/list}
+ *
+ * @param {object=} options - Network search options.
+ * @param {boolean} options.autoPaginate - Have pagination handled
+ * automatically. Default: true.
+ * @param {string} options.filter - Search filter in the format of
+ * `{name} {comparison} {filterString}`.
+ * - **`name`**: the name of the field to compare
+ * - **`comparison`**: the comparison operator, `eq` (equal) or `ne`
+ * (not equal)
+ * - **`filterString`**: the string to filter to. For string fields, this
+ * can be a regular expression.
+ * @param {number} options.maxResults - Maximum number of networks to return.
+ * @param {string} options.pageToken - A previously-returned page token
+ * representing part of the larger set of results to view.
+ * @param {function} callback - The callback function.
+ * @param {?error} callback.err - An error returned while making this request.
+ * @param {module:compute/network} callback.networks - Network objects from your
+ * project.
+ * @param {?object} callback.nextQuery - If present, query with this object to
+ * check for more results.
+ * @param {object} callback.apiResponse - The full API response.
+ *
+ * @example
+ * gce.getNetworks(function(err, networks) {
+ * // `networks` is an array of `Network` objects.
+ * });
+ *
+ * //-
+ * // To control how many API requests are made and page through the results
+ * // manually, set `autoPaginate` to `false`.
+ * //-
+ * function callback(err, networks, nextQuery, apiResponse) {
+ * if (nextQuery) {
+ * // More results exist.
+ * gce.getNetworks(nextQuery, callback);
+ * }
+ * }
+ *
+ * gce.getNetworks({
+ * autoPaginate: false
+ * }, callback);
+ *
+ * //-
+ * // Get the networks from your project as a readable object stream.
+ * //-
+ * gce.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.
+ * //-
+ * gce.getNetworks()
+ * .on('data', function(network) {
+ * this.end();
+ * });
+ */
+Compute.prototype.getNetworks = function(options, callback) {
+ if (is.fn(options)) {
+ callback = options;
+ options = {};
+ }
+
+ options = options || {};
+
+ var self = this;
+ this.makeReq_('GET', '/global/networks', options, null, function(err, resp) {
+ if (err) {
+ callback(err, null, null, resp);
+ return;
+ }
+
+ var nextQuery = null;
+
+ if (resp.nextPageToken) {
+ nextQuery = extend({}, options, {
+ pageToken: resp.nextPageToken
+ });
+ }
+
+ var networks = (resp.items || []).map(function(network) {
+ var networkInstance = self.network(network.name);
+ networkInstance.metadata = network;
+ return networkInstance;
+ });
+
+ callback(null, networks, nextQuery, resp);
+ });
+};
+
+/**
+ * Get a list of global operations.
+ *
+ * @resource [Global Operation Overview]{@link https://cloud.google.com/compute/docs/reference/v1/globalOperations}
+ * @resource [GlobalOperations: list API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/globalOperations/list}
+ *
+ * @param {object=} options - Operation search options.
+ * @param {boolean} options.autoPaginate - Have pagination handled
+ * automatically. Default: true.
+ * @param {string} options.filter - Search filter in the format of
+ * `{name} {comparison} {filterString}`.
+ * - **`name`**: the name of the field to compare
+ * - **`comparison`**: the comparison operator, `eq` (equal) or `ne`
+ * (not equal)
+ * - **`filterString`**: the string to filter to. For string fields, this
+ * can be a regular expression.
+ * @param {number} options.maxResults - Maximum number of operations to return.
+ * @param {string} options.pageToken - A previously-returned page token
+ * representing part of the larger set of results to view.
+ * @param {function} callback - The callback function.
+ * @param {?error} callback.err - An error returned while making this request.
+ * @param {module:compute/operation} callback.operations - Operation objects
+ * from your project.
+ * @param {?object} callback.nextQuery - If present, query with this object to
+ * check for more results.
+ * @param {object} callback.apiResponse - The full API response.
+ *
+ * @example
+ * gce.getOperations(function(err, operations) {
+ * // `operations` is an array of `Operation` objects.
+ * });
+ *
+ * //-
+ * // To control how many API requests are made and page through the results
+ * // manually, set `autoPaginate` to `false`.
+ * //-
+ * function callback(err, operations, nextQuery, apiResponse) {
+ * if (nextQuery) {
+ * // More results exist.
+ * gce.getOperations(nextQuery, callback);
+ * }
+ * }
+ *
+ * gce.getOperations({
+ * autoPaginate: false
+ * }, callback);
+ *
+ * //-
+ * // Get the operations from your project as a readable object stream.
+ * //-
+ * gce.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.
+ * //-
+ * gce.getOperations()
+ * .on('data', function(operation) {
+ * this.end();
+ * });
+ */
+Compute.prototype.getOperations = function(options, callback) {
+ var self = this;
+
+ if (is.fn(options)) {
+ callback = options;
+ options = {};
+ }
+
+ options = options || {};
+
+ var path = '/global/operations';
+
+ this.makeReq_('GET', path, options, null, function(err, resp) {
+ if (err) {
+ callback(err, null, null, resp);
+ return;
+ }
+
+ var nextQuery = null;
+
+ if (resp.nextPageToken) {
+ nextQuery = extend({}, options, {
+ pageToken: resp.nextPageToken
+ });
+ }
+
+ var operations = (resp.items || []).map(function(operation) {
+ var operationInstance = self.operation(operation.name);
+ operationInstance.metadata = operation;
+ return operationInstance;
+ });
+
+ callback(null, operations, nextQuery, resp);
+ });
+};
+
+/**
+ * Return the regions available to your project.
+ *
+ * @resource [Regions & Zones Overview]{@link https://cloud.google.com/compute/docs/zones}
+ * @resource [Regions: list API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/regions/list}
+ *
+ * @param {object=} options - Instance search options.
+ * @param {boolean} options.autoPaginate - Have pagination handled
+ * automatically. Default: true.
+ * @param {string} options.filter - Search filter in the format of
+ * `{name} {comparison} {filterString}`.
+ * - **`name`**: the name of the field to compare
+ * - **`comparison`**: the comparison operator, `eq` (equal) or `ne`
+ * (not equal)
+ * - **`filterString`**: the string to filter to. For string fields, this
+ * can be a regular expression.
+ * @param {number} options.maxResults - Maximum number of instances to return.
+ * @param {string} options.pageToken - A previously-returned page token
+ * representing part of the larger set of results to view.
+ * @param {function} callback - The callback function.
+ * @param {?error} callback.err - An error returned while making this request.
+ * @param {module:compute/region} callback.regions - Region objects that are
+ * available to your project.
+ * @param {?object} callback.nextQuery - If present, query with this object to
+ * check for more results.
+ * @param {object} callback.apiResponse - The full API response.
+ *
+ * @example
+ * gce.getRegions(function(err, regions) {
+ * // `regions` is an array of `Region` objects.
+ * });
+ *
+ * //-
+ * // To control how many API requests are made and page through the results
+ * // manually, set `autoPaginate` to `false`.
+ * //-
+ * function callback(err, regions, nextQuery, apiResponse) {
+ * if (nextQuery) {
+ * // More results exist.
+ * gce.getRegions(nextQuery, callback);
+ * }
+ * }
+ *
+ * gce.getRegions({
+ * autoPaginate: false
+ * }, callback);
+ *
+ * //-
+ * // Get the regions available to your project as a readable object stream.
+ * //-
+ * gce.getRegions()
+ * .on('error', console.error)
+ * .on('data', function(region) {
+ * // `region` is a `Region` object.
+ * })
+ * .on('end', function() {
+ * // All regions retrieved.
+ * });
+ *
+ * //-
+ * // If you anticipate many results, you can end a stream early to prevent
+ * // unnecessary processing and API requests.
+ * //-
+ * gce.getRegions()
+ * .on('data', function(region) {
+ * this.end();
+ * });
+ */
+Compute.prototype.getRegions = function(options, callback) {
+ var self = this;
+
+ if (is.fn(options)) {
+ callback = options;
+ options = {};
+ }
+
+ this.makeReq_('GET', '/regions', options, null, function(err, resp) {
+ if (err) {
+ callback(err, null, null, resp);
+ return;
+ }
+
+ var nextQuery = null;
+
+ if (resp.nextPageToken) {
+ nextQuery = extend({}, options, {
+ pageToken: resp.nextPageToken
+ });
+ }
+
+ var regions = resp.items.map(function(region) {
+ var regionInstance = self.region(region.name);
+ regionInstance.metadata = region;
+ return regionInstance;
+ });
+
+ callback(null, regions, nextQuery, resp);
+ });
+};
+
+/**
+ * Get a list of snapshots.
+ *
+ * @resource [Snapshots Overview]{@link https://cloud.google.com/compute/docs/disks/persistent-disks#snapshots}
+ * @resource [Snapshots: list API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/snapshots/list}
+ *
+ * @param {object=} options - Snapshot search options.
+ * @param {boolean} options.autoPaginate - Have pagination handled
+ * automatically. Default: true.
+ * @param {string} options.filter - Search filter in the format of
+ * `{name} {comparison} {filterString}`.
+ * - **`name`**: the name of the field to compare
+ * - **`comparison`**: the comparison operator, `eq` (equal) or `ne`
+ * (not equal)
+ * - **`filterString`**: the string to filter to. For string fields, this
+ * can be a regular expression.
+ * @param {number} options.maxResults - Maximum number of snapshots to return.
+ * @param {string} options.pageToken - A previously-returned page token
+ * representing part of the larger set of results to view.
+ * @param {function} callback - The callback function.
+ * @param {?error} callback.err - An error returned while making this request.
+ * @param {module:compute/snapshot} callback.snapshots - Snapshot objects from
+ * your project.
+ * @param {?object} callback.nextQuery - If present, query with this object to
+ * check for more results.
+ * @param {object} callback.apiResponse - The full API response.
+ *
+ * @example
+ * gce.getSnapshots(function(err, snapshots) {
+ * // `snapshots` is an array of `Snapshot` objects.
+ * });
+ *
+ * //-
+ * // To control how many API requests are made and page through the results
+ * // manually, set `autoPaginate` to `false`.
+ * //-
+ * function callback(err, snapshots, nextQuery, apiResponse) {
+ * if (nextQuery) {
+ * // More results exist.
+ * gce.getSnapshots(nextQuery, callback);
+ * }
+ * }
+ *
+ * gce.getSnapshots({
+ * autoPaginate: false
+ * }, callback);
+ *
+ * //-
+ * // Get the snapshots from your project as a readable object stream.
+ * //-
+ * gce.getSnapshots()
+ * .on('error', console.error)
+ * .on('data', function(snapshot) {
+ * // `snapshot` is a `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.
+ * //-
+ * gce.getSnapshots()
+ * .on('data', function(snapshot) {
+ * this.end();
+ * });
+ */
+Compute.prototype.getSnapshots = function(options, callback) {
+ var self = this;
+
+ if (is.fn(options)) {
+ callback = options;
+ options = {};
+ }
+
+ options = options || {};
+
+
+ this.makeReq_('GET', '/global/snapshots', options, null, function(err, resp) {
+ if (err) {
+ callback(err, null, null, resp);
+ return;
+ }
+
+ var nextQuery = null;
+
+ if (resp.nextPageToken) {
+ nextQuery = extend({}, options, {
+ pageToken: resp.nextPageToken
+ });
+ }
+
+ var snapshots = (resp.items || []).map(function(snapshot) {
+ var snapshotInstance = self.snapshot(snapshot.name);
+ snapshotInstance.metadata = snapshot;
+ return snapshotInstance;
+ });
+
+ callback(null, snapshots, nextQuery, resp);
+ });
+};
+
+/**
+ * Get a list of virtual machine instances.
+ *
+ * @resource [Instances and Networks]{@link https://cloud.google.com/compute/docs/instances-and-network}
+ * @resource [Instances: aggregatedList API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/instances/aggregatedList}
+ *
+ * @param {object=} options - Instance search options.
+ * @param {boolean} options.autoPaginate - Have pagination handled
+ * automatically. Default: true.
+ * @param {string} options.filter - Search filter in the format of
+ * `{name} {comparison} {filterString}`.
+ * - **`name`**: the name of the field to compare
+ * - **`comparison`**: the comparison operator, `eq` (equal) or `ne`
+ * (not equal)
+ * - **`filterString`**: the string to filter to. For string fields, this
+ * can be a regular expression.
+ * @param {number} options.maxResults - Maximum number of instances to return.
+ * @param {string} options.pageToken - A previously-returned page token
+ * representing part of the larger set of results to view.
+ * @param {function} callback - The callback function.
+ * @param {?error} callback.err - An error returned while making this request.
+ * @param {module:compute/vm} callback.vms - VM objects from your project.
+ * @param {?object} callback.nextQuery - If present, query with this object to
+ * check for more results.
+ * @param {object} callback.apiResponse - The full API response.
+ *
+ * @example
+ * gce.getVMs(function(err, vms) {
+ * // `vms` is an array of `VM` objects.
+ * });
+ *
+ * //-
+ * // To control how many API requests are made and page through the results
+ * // manually, set `autoPaginate` to `false`.
+ * //-
+ * function callback(err, vms, nextQuery, apiResponse) {
+ * if (nextQuery) {
+ * // More results exist.
+ * gce.getVMs(nextQuery, callback);
+ * }
+ * }
+ *
+ * gce.getVMs({
+ * autoPaginate: false
+ * }, callback);
+ *
+ * //-
+ * // Get the VM instances from your project as a readable object stream.
+ * //-
+ * gce.getVMs()
+ * .on('error', console.error)
+ * .on('data', function(vm) {
+ * // `vm` is a `VM` object.
+ * })
+ * .on('end', function() {
+ * // All vms retrieved.
+ * });
+ *
+ * //-
+ * // If you anticipate many results, you can end a stream early to prevent
+ * // unnecessary processing and API requests.
+ * //-
+ * gce.getVMs()
+ * .on('data', function(vm) {
+ * this.end();
+ * });
+ */
+Compute.prototype.getVMs = function(options, callback) {
+ var self = this;
+
+ if (is.fn(options)) {
+ callback = options;
+ options = {};
+ }
+
+ options = options || {};
+
+ var path = '/aggregated/instances';
+
+ this.makeReq_('GET', path, options, null, function(err, resp) {
+ if (err) {
+ callback(err, null, null, resp);
+ return;
+ }
+
+ var nextQuery = null;
+
+ if (resp.nextPageToken) {
+ nextQuery = extend({}, options, {
+ pageToken: resp.nextPageToken
+ });
+ }
+
+ var zones = resp.items || {};
+
+ var vms = Object.keys(zones).reduce(function(acc, zoneName) {
+ var zone = self.zone(zoneName.replace('zones/', ''));
+ var instances = zones[zoneName].instances || [];
+
+ instances.forEach(function(instance) {
+ var vmInstance = zone.vm(instance.name);
+ vmInstance.metadata = instance;
+ acc.push(vmInstance);
+ });
+
+ return acc;
+ }, []);
+
+ callback(null, vms, nextQuery, resp);
+ });
+};
+
+/**
+ * Return the zones available to your project.
+ *
+ * @resource [Regions & Zones Overview]{@link https://cloud.google.com/compute/docs/zones}
+ * @resource [Zones: list API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/zones/list}
+ *
+ * @param {object=} options - Instance search options.
+ * @param {boolean} options.autoPaginate - Have pagination handled
+ * automatically. Default: true.
+ * @param {string} options.filter - Search filter in the format of
+ * `{name} {comparison} {filterString}`.
+ * - **`name`**: the name of the field to compare
+ * - **`comparison`**: the comparison operator, `eq` (equal) or `ne`
+ * (not equal)
+ * - **`filterString`**: the string to filter to. For string fields, this
+ * can be a regular expression.
+ * @param {number} options.maxResults - Maximum number of instances to return.
+ * @param {string} options.pageToken - A previously-returned page token
+ * representing part of the larger set of results to view.
+ * @param {function} callback - The callback function.
+ * @param {?error} callback.err - An error returned while making this request.
+ * @param {module:compute/zone} callback.zones - Zone objects that are available
+ * to your project.
+ * @param {?object} callback.nextQuery - If present, query with this object to
+ * check for more results.
+ * @param {object} callback.apiResponse - The full API response.
+ *
+ * @example
+ * gce.getZones(function(err, zones) {
+ * // `zones` is an array of `Zone` objects.
+ * });
+ *
+ * //-
+ * // To control how many API requests are made and page through the results
+ * // manually, set `autoPaginate` to `false`.
+ * //-
+ * function callback(err, zones, nextQuery, apiResponse) {
+ * if (nextQuery) {
+ * // More results exist.
+ * gce.getZones(nextQuery, callback);
+ * }
+ * }
+ *
+ * gce.getZones({
+ * autoPaginate: false
+ * }, callback);
+ *
+ * //-
+ * // Get the zones available to your project as a readable object stream.
+ * //-
+ * gce.getZones()
+ * .on('error', console.error)
+ * .on('data', function(zone) {
+ * // `zone` is a `Zone` object.
+ * })
+ * .on('end', function() {
+ * // All zones retrieved.
+ * });
+ *
+ * //-
+ * // If you anticipate many results, you can end a stream early to prevent
+ * // unnecessary processing and API requests.
+ * //-
+ * gce.getZones()
+ * .on('data', function(zone) {
+ * this.end();
+ * });
+ */
+Compute.prototype.getZones = function(options, callback) {
+ var self = this;
+
+ if (is.fn(options)) {
+ callback = options;
+ options = {};
+ }
+
+ this.makeReq_('GET', '/zones', options, null, function(err, resp) {
+ if (err) {
+ callback(err, null, null, resp);
+ return;
+ }
+
+ var nextQuery = null;
+
+ if (resp.nextPageToken) {
+ nextQuery = extend({}, options, {
+ pageToken: resp.nextPageToken
+ });
+ }
+
+ var zones = resp.items.map(function(zone) {
+ var zoneInstance = self.zone(zone.name);
+ zoneInstance.metadata = zone;
+ return zoneInstance;
+ });
+
+ callback(null, zones, nextQuery, resp);
+ });
+};
+
+/**
+ * Get a reference to a Google Compute Engine network.
+ *
+ * @resource [Networks Overview]{@link https://cloud.google.com/compute/docs/networking#networks}
+ *
+ * @param {string} name - Name of the existing network.
+ * @return {module:compute/network}
+ *
+ * @example
+ * var network = gce.network('network-name');
+ */
+Compute.prototype.network = function(name) {
+ return new Network(this, name);
+};
+
+/**
+ * Get a reference to a global Google Compute Engine operation.
+ *
+ * @resource [Global Operation Overview]{@link https://cloud.google.com/compute/docs/reference/v1/globalOperations}
+ *
+ * @param {string} name - Name of the existing operation.
+ * @return {module:compute/operation}
+ *
+ * @example
+ * var operation = gce.operation('operation-name');
+ */
+Compute.prototype.operation = function(name) {
+ return new Operation(this, name);
+};
+
+/**
+ * Get a reference to a Google Compute Engine region.
+ *
+ * @resource [Regions & Zones Overview]{@link https://cloud.google.com/compute/docs/zones}
+ *
+ * @param {string} name - Name of the region.
+ * @return {module:compute/region}
+ *
+ * @example
+ * var region = gce.region('region-name');
+ */
+Compute.prototype.region = function(name) {
+ return new Region(this, name);
+};
+
+/**
+ * Get a reference to a Google Compute Engine snapshot.
+ *
+ * @resource [Snapshots Overview]{@link https://cloud.google.com/compute/docs/disks/persistent-disks#snapshots}
+ *
+ * @param {string} name - Name of the existing snapshot.
+ * @return {module:compute/snapshot}
+ *
+ * @example
+ * var snapshot = gce.snapshot('snapshot-name');
+ */
+Compute.prototype.snapshot = function(name) {
+ return new Snapshot(this, name);
+};
+
+/**
+ * Get a reference to a Google Compute Engine zone.
+ *
+ * @resource [Regions & Zones Overview]{@link https://cloud.google.com/compute/docs/zones}
+ *
+ * @param {string} name - Name of the zone.
+ * @return {module:compute/zone}
+ *
+ * @example
+ * var zone = gce.zone('zone-name');
+ */
+Compute.prototype.zone = function(name) {
+ return new Zone(this, name);
+};
+
+/**
+ * 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);
+};
+
+/*! 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, [
+ 'getAddresses',
+ 'getDisks',
+ 'getFirewalls',
+ 'getNetworks',
+ 'getOperations',
+ 'getRegions',
+ 'getSnapshots',
+ 'getVMs',
+ 'getZones'
+]);
+
+module.exports = Compute;
diff --git a/lib/compute/network.js b/lib/compute/network.js
new file mode 100644
index 00000000000..3ec6d06cfb9
--- /dev/null
+++ b/lib/compute/network.js
@@ -0,0 +1,307 @@
+/*!
+ * Copyright 2015 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';
+
+var extend = require('extend');
+var format = require('string-format-obj');
+var is = require('is');
+
+/**
+ * @type {module:common/util}
+ * @private
+ */
+var util = require('../common/util.js');
+
+/*! Developer Documentation
+ *
+ * @param {module:compute} compute - The Compute module this network belongs to.
+ * @param {string} name - Network name.
+ */
+/**
+ * A Network object allows you to interact with a Google Compute Engine network.
+ *
+ * @resource [Networks Overview]{@link https://cloud.google.com/compute/docs/networking#networks}
+ * @resource [Network Resource]{@link https://cloud.google.com/compute/docs/reference/v1/networks}
+ *
+ * @constructor
+ * @alias module:compute/network
+ *
+ * @example
+ * var gcloud = require('gcloud')({
+ * keyFilename: '/path/to/keyfile.json',
+ * projectId: 'grape-spaceship-123'
+ * });
+ *
+ * var gce = gcloud.compute();
+ *
+ * var network = gce.network('network-name');
+ */
+function Network(compute, name) {
+ this.compute = compute;
+ this.name = name;
+ this.metadata = {};
+
+ this.formattedName = Network.formatName_(compute, name);
+}
+
+/**
+ * Format a network's name how the API expects.
+ *
+ * @param {module:compute} compute - The Compute object this network belongs to.
+ * @param {string} name - The name of the network.
+ * @return {string} - The formatted name.
+ */
+Network.formatName_ = function(compute, name) {
+ return format('projects/{projectId}/global/networks/{name}', {
+ projectId: compute.projectId,
+ name: name
+ });
+};
+
+/**
+ * Create a firewall for this network.
+ *
+ * @resource [Firewalls Overview]{@link https://cloud.google.com/compute/docs/networking#firewalls}
+ * @resource [Firewalls: insert API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/firewalls/insert}
+ *
+ * @param {string} name - Name of the firewall.
+ * @param {object} config - See a
+ * [Firewall resource](https://cloud.google.com/compute/docs/reference/v1/firewalls#resource).
+ * @param {object} config.protocols - A map of protocol to port range. The keys
+ * of the object refer to a protocol (e.g. `tcp`, `udp`) and the value for
+ * the key are the ports/port-ranges that are allowed to make a connection.
+ * @param {string[]} config.ranges - The IP address blocks that this rule
+ * applies to, expressed in
+ * [CIDR](http://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing)
+ * format.
+ * @param {function} callback - The callback function.
+ * @param {?error} callback.err - An error returned while making this request.
+ * @param {module:compute/firewall} callback.firewall - The created Firewall
+ * object.
+ * @param {module:compute/operation} callback.operation - An operation object
+ * that can be used to check the status of the request.
+ * @param {object} callback.apiResponse - The full API response.
+ *
+ * @example
+ * var config = {
+ * protocols: {
+ * tcp: [3000],
+ * udp: [] // An empty array means all ports are allowed.
+ * },
+ *
+ * ranges: ['0.0.0.0/0']
+ * };
+ *
+ * function callback(err, firewall, operation, apiResponse) {
+ * // `firewall` is a Firewall object.
+ *
+ * // `operation` is an Operation object that can be used to check the status
+ * // of the request.
+ * }
+ *
+ * network.createFirewall('new-firewall-name', config, callback);
+ */
+Network.prototype.createFirewall = function(name, config, callback) {
+ config = extend({}, config, {
+ network: this.formattedName
+ });
+
+ this.compute.createFirewall(name, config, callback);
+};
+
+/**
+ * Delete the network.
+ *
+ * @resource [Networks: delete API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/networks/delete}
+ *
+ * @param {function=} callback - The callback function.
+ * @param {?error} callback.err - An error returned while making this request.
+ * @param {module:compute/operation} callback.operation - An operation object
+ * that can be used to check the status of the request.
+ * @param {object} callback.apiResponse - The full API response.
+ *
+ * @example
+ * network.delete(function(err, operation, apiResponse) {
+ * // `operation` is an Operation object that can be used to check the status
+ * // of the request.
+ * });
+ */
+Network.prototype.delete = function(callback) {
+ var compute = this.compute;
+
+ callback = callback || util.noop;
+
+ this.makeReq_('DELETE', '', null, null, function(err, resp) {
+ if (err) {
+ callback(err, null, resp);
+ return;
+ }
+
+ var operation = compute.operation(resp.name);
+ operation.metadata = resp;
+
+ callback(null, operation, resp);
+ });
+};
+
+/**
+ * Get a reference to a Google Compute Engine firewall in this network.
+ *
+ * @resource [Firewalls Overview]{@link https://cloud.google.com/compute/docs/networking#firewalls}
+ *
+ * @param {string} name - Name of the existing firewall.
+ *
+ * @example
+ * var firewall = network.firewall('firewall-name');
+ */
+Network.prototype.firewall = function(name) {
+ var firewall = this.compute.firewall(name);
+
+ firewall.metadata = {
+ network: this.formattedName
+ };
+
+ return firewall;
+};
+
+/**
+ * Get a list of firewalls for this network.
+ *
+ * @resource [Firewalls Overview]{@link https://cloud.google.com/compute/docs/networking#firewalls}
+ * @resource [Firewalls: list API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/firewalls/list}
+ *
+ * @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} options.pageToken - A previously-returned page token
+ * representing part of the larger set of results to view.
+ * @param {function} callback - The callback function.
+ * @param {?error} callback.err - An error returned while making this request.
+ * @param {module:compute/firewall} callback.firewalls - Firewall objects from
+ * this network.
+ * @param {?object} callback.nextQuery - If present, query with this object to
+ * check for more results.
+ * @param {object} callback.apiResponse - The full API response.
+ *
+ * @example
+ * network.getFirewalls(function(err, firewalls) {
+ * // `firewalls` is an array of `Firewall` objects.
+ * });
+ *
+ * //-
+ * // To control how many API requests are made and page through the results
+ * // manually, set `autoPaginate` to `false`.
+ * //-
+ * function callback(err, firewalls, nextQuery, apiResponse) {
+ * if (nextQuery) {
+ * // More results exist.
+ * network.getFirewalls(nextQuery, callback);
+ * }
+ * }
+ *
+ * network.getFirewalls({
+ * autoPaginate: false
+ * }, callback);
+ *
+ * //-
+ * // Get the firewalls from your project as a readable object stream.
+ * //-
+ * network.getFirewalls()
+ * .on('error', console.error)
+ * .on('data', function(firewall) {
+ * // `firewall` is a `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.
+ * //-
+ * network.getFirewalls()
+ * .on('data', function(firewall) {
+ * this.end();
+ * });
+ */
+Network.prototype.getFirewalls = function(options, callback) {
+ if (is.fn(options)) {
+ callback = options;
+ options = {};
+ }
+
+ options = extend({}, options, {
+ filter: 'network eq .*' + this.formattedName
+ });
+
+ return this.compute.getFirewalls(options, callback);
+};
+
+/**
+ * Get the network's metadata.
+ *
+ * @resource [Network Resource]{@link https://cloud.google.com/compute/docs/reference/v1/networks}
+ * @resource [Networks: get API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/networks/delete}
+ *
+ * @param {function=} callback - The callback function.
+ * @param {?error} callback.err - An error returned while making this request.
+ * @param {object} callback.metadata - The network's metadata.
+ * @param {object} callback.apiResponse - The full API response.
+ *
+ * @example
+ * network.getMetadata(function(err, metadata, apiResponse) {});
+ */
+Network.prototype.getMetadata = function(callback) {
+ var self = this;
+
+ callback = callback || util.noop;
+
+ this.makeReq_('GET', '', null, null, function(err, resp) {
+ if (err) {
+ callback(err, null, resp);
+ 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.
+ */
+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;
diff --git a/lib/compute/operation.js b/lib/compute/operation.js
new file mode 100644
index 00000000000..a26c2cf412d
--- /dev/null
+++ b/lib/compute/operation.js
@@ -0,0 +1,246 @@
+/*!
+ * Copyright 2015 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';
+
+var extend = require('extend');
+var is = require('is');
+
+/**
+ * @type {module:common/util}
+ * @private
+ */
+var util = require('../common/util.js');
+
+/*! Developer Documentation
+ *
+ * @param {module:compute} scope - The scope of the operation: a `Compute`,
+ * `Zone`, or `Region` object.
+ * @param {string} name - Operation name.
+ */
+/**
+ * An Operation object allows you to interact with a Google Compute Engine
+ * operation.
+ *
+ * An operation can be a
+ * [GlobalOperation](https://cloud.google.com/compute/docs/reference/v1/globalOperations),
+ * [RegionOperation](https://cloud.google.com/compute/docs/reference/v1/regionOperations),
+ * or
+ * [ZoneOperation](https://cloud.google.com/compute/docs/reference/v1/zoneOperations).
+ *
+ * @constructor
+ * @alias module:compute/operation
+ *
+ * @example
+ * var gcloud = require('gcloud')({
+ * keyFilename: '/path/to/keyfile.json',
+ * projectId: 'grape-spaceship-123'
+ * });
+ *
+ * var gce = gcloud.compute();
+ *
+ * //-
+ * // Reference a global operation.
+ * //-
+ * var operation = gce.operation('operation-id');
+ *
+ * //-
+ * // Reference a region operation.
+ * //-
+ * var region = gce.region('us-central1');
+ * var operation = region.operation('operation-id');
+ *
+ * //-
+ * // Reference a zone operation.
+ * //-
+ * var zone = gce.zone('us-central1-a');
+ * var operation = zone.operation('operation-id');
+ */
+function Operation(scope, name) {
+ this.scope = scope;
+ this.name = name;
+ this.metadata = {};
+}
+
+/**
+ * Delete the operation.
+ *
+ * @resource [GlobalOperations: delete API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/globalOperations/delete}
+ * @resource [RegionOperations: delete API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/regionOperations/delete}
+ * @resource [ZoneOperations: delete API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/zoneOperations/delete}
+ *
+ * @param {function=} callback - The callback function.
+ * @param {?error} callback.err - An error returned while making this request.
+ * @param {object} callback.apiResponse - The full API response.
+ *
+ * @example
+ * operation.delete(function(err, apiResponse) {});
+ */
+Operation.prototype.delete = function(callback) {
+ callback = callback || util.noop;
+
+ this.makeReq_('DELETE', '', null, null, function(err, resp) {
+ callback(err, resp);
+ });
+};
+
+/**
+ * Get the operation's metadata. For a detailed description of metadata see
+ * [Operation resource](https://goo.gl/sWm1rt).
+ *
+ * @resource [GlobalOperations: get API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/globalOperations/get}
+ * @resource [RegionOperations: get API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/regionOperations/get}
+ * @resource [ZoneOperations: get API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/zoneOperations/get}
+ *
+ * @param {function=} callback - The callback function.
+ * @param {?error} callback.err - An error returned while making this request
+ * @param {object} callback.metadata - The disk's metadata.
+ * @param {object} callback.apiResponse - The full API response.
+ *
+ * @example
+ * operation.getMetadata(function(err, metadata, apiResponse) {
+ * // `metadata.error`: Contains errors if the operation failed.
+ * // `metadata.warnings`: Contains warnings.
+ * });
+ */
+Operation.prototype.getMetadata = function(callback) {
+ var self = this;
+
+ callback = callback || util.noop;
+
+ this.makeReq_('GET', '', null, null, function(err, resp) {
+ // An Operation entity contains a property named `error`. This makes
+ // `makeReq_` think the operation failed, and will return an ApiError to
+ // this callback. We have to make sure this isn't a false error by seeing if
+ // the response body contains a property that wouldn't exist on a failed API
+ // request (`name`).
+ var isActualError = err && (!resp || resp.name !== self.name);
+
+ if (isActualError) {
+ callback(err, null, resp);
+ return;
+ }
+
+ self.metadata = resp;
+
+ callback(null, self.metadata, resp);
+ });
+};
+
+/**
+ * Register a callback for when the operation is complete.
+ *
+ * If the operation doesn't complete after the maximum number of attempts have
+ * been made (see `options.maxAttempts` and `options.interval`), an error will
+ * be provided to your callback with code: `OPERATION_INCOMPLETE`.
+ *
+ * @param {object=} options - Configuration object.
+ * @param {number} options.maxAttempts - Maximum number of attempts to make an
+ * API request to check if the operation is complete. (Default: `10`)
+ * @param {number} options.interval - Amount of time in milliseconds between
+ * each request. (Default: `3000`)
+ * @param {function} callback - The callback function.
+ * @param {?error} callback.err - An error returned while making this request.
+ * @param {object} callback.metadata - The operation's metadata.
+ *
+ * @example
+ * operation.onComplete(function(err, metadata) {
+ * if (err.code === 'OPERATION_INCOMPLETE') {
+ * // The operation is not complete yet. You may want to register another
+ * // `onComplete` listener or queue for later.
+ * }
+ *
+ * if (!err) {
+ * // Operation complete!
+ * }
+ * });
+ */
+Operation.prototype.onComplete = function(options, callback) {
+ var self = this;
+
+ if (is.fn(options)) {
+ callback = options;
+ options = {};
+ }
+
+ options = extend({
+ maxAttempts: 10,
+ interval: 3000
+ }, options);
+
+ var didNotCompleteError = new Error('Operation did not complete.');
+ didNotCompleteError.code = 'OPERATION_INCOMPLETE';
+
+ var numAttempts = 0;
+
+ function checkMetadata() {
+ numAttempts++;
+
+ if (numAttempts > options.maxAttempts) {
+ callback(didNotCompleteError, self.metadata);
+ return;
+ }
+
+ setTimeout(function() {
+ self.getMetadata(onMetadata);
+ }, options.interval);
+ }
+
+ function onMetadata(err, metadata) {
+ if (err) {
+ callback(err, metadata);
+ return;
+ }
+
+ if (metadata.status !== 'DONE') {
+ checkMetadata();
+ return;
+ }
+
+ // The operation is complete.
+ callback(null, metadata);
+ }
+
+ checkMetadata();
+};
+
+/**
+ * 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.name === 'Compute') {
+ path = '/global' + path;
+ }
+
+ this.scope.makeReq_(method, path, query, body, callback);
+};
+
+module.exports = Operation;
diff --git a/lib/compute/region.js b/lib/compute/region.js
new file mode 100644
index 00000000000..50178338be3
--- /dev/null
+++ b/lib/compute/region.js
@@ -0,0 +1,424 @@
+/*!
+ * Copyright 2015 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';
+
+var extend = require('extend');
+var is = require('is');
+
+/**
+ * @type {module:compute/address}
+ * @private
+ */
+var Address = require('./address.js');
+
+/**
+ * @type {module:compute/operation}
+ * @private
+ */
+var Operation = require('./operation.js');
+
+/**
+ * @type {module:common/streamrouter}
+ * @private
+ */
+var streamRouter = require('../common/stream-router.js');
+
+/**
+ * @type {module:common/util}
+ * @private
+ */
+var util = require('../common/util.js');
+
+/*! Developer Documentation
+ *
+ * @param {module:compute} compute - Compute object this region belongs to.
+ * @param {string} name - Name of the region.
+ */
+/**
+ * A Region object allows you to interact with a Google Compute Engine region.
+ *
+ * @resource [Regions & Zones Overview]{@link https://cloud.google.com/compute/docs/zones}
+ * @resource [Region Resource]{@link https://cloud.google.com/compute/docs/reference/v1/regions}
+ *
+ * @constructor
+ * @alias module:compute/region
+ *
+ * @example
+ * var gcloud = require('gcloud')({
+ * keyFilename: '/path/to/keyfile.json',
+ * projectId: 'grape-spaceship-123'
+ * });
+ *
+ * var gce = gcloud.compute();
+ *
+ * var region = gce.region('us-central1');
+ */
+function Region(compute, name) {
+ this.compute = compute;
+ this.name = name;
+ this.metadata = {};
+}
+
+/**
+ * Get a reference to a Google Compute Engine address in this region.
+ *
+ * @resource [Instances and Networks]{@link https://cloud.google.com/compute/docs/instances-and-network}
+ *
+ * @param {string} name - Name of the existing address.
+ * @return {module:compute/address}
+ *
+ * @example
+ * var address = region.address('address-name');
+ */
+Region.prototype.address = function(name) {
+ return new Address(this, name);
+};
+
+/**
+ * Create an address in this region.
+ *
+ * @resource [Instances and Networks]{@link https://cloud.google.com/compute/docs/instances-and-network}
+ * @resource [Address Resource]{@link https://cloud.google.com/compute/docs/reference/v1/addresses}
+ * @resource [Addresses: insert API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/addresses/insert}
+ *
+ * @param {string} name - Name of the address.
+ * @param {object=} options - See an
+ * [Address resource](https://cloud.google.com/compute/docs/reference/v1/addresses).
+ * @param {function} callback - The callback function.
+ * @param {?error} callback.err - An error returned while making this request.
+ * @param {module:compute/address} callback.address - The created Address
+ * object.
+ * @param {module:compute/operation} callback.operation - An operation object
+ * that can be used to check the status of the request.
+ * @param {object} callback.apiResponse - The full API response.
+ *
+ * @example
+ * function callback(err, address, operation, apiResponse) {
+ * // `address` is an Address object.
+ *
+ * // `operation` is an Operation object that can be used to check the status
+ * // of the request.
+ * }
+ *
+ * region.createAddress('new-address', callback);
+ */
+Region.prototype.createAddress = function(name, options, callback) {
+ var self = this;
+
+ if (is.fn(options)) {
+ callback = options;
+ options = {};
+ }
+
+ var body = extend({}, options, {
+ name: name
+ });
+
+ this.makeReq_('POST', '/addresses', null, body, function(err, resp) {
+ if (err) {
+ callback(err, null, null, resp);
+ return;
+ }
+
+ var address = self.address(name);
+
+ var operation = self.operation(resp.name);
+ operation.metadata = resp;
+
+ callback(null, address, operation, resp);
+ });
+};
+
+/**
+ * Get a list of addresses in this region.
+ *
+ * @resource [Instances and Networks]{@link https://cloud.google.com/compute/docs/instances-and-network}
+ * @resource [Addresses: list API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/addresses/list}
+ *
+ * @param {object=} options - Address search options.
+ * @param {boolean} options.autoPaginate - Have pagination handled
+ * automatically. Default: true.
+ * @param {string} options.filter - Search filter in the format of
+ * `{name} {comparison} {filterString}`.
+ * - **`name`**: the name of the field to compare
+ * - **`comparison`**: the comparison operator, `eq` (equal) or `ne`
+ * (not equal)
+ * - **`filterString`**: the string to filter to. For string fields, this
+ * can be a regular expression.
+ * @param {number} options.maxResults - Maximum number of addresses to return.
+ * @param {string} options.pageToken - A previously-returned page token
+ * representing part of the larger set of results to view.
+ * @param {function} callback - The callback function.
+ * @param {?error} callback.err - An error returned while making this request.
+ * @param {module:compute/address} callback.addresses - Address objects from
+ * this region.
+ * @param {?object} callback.nextQuery - If present, query with this object to
+ * check for more results.
+ * @param {object} callback.apiResponse - The full API response.
+ *
+ * @example
+ * region.getAddresses(function (err, addresses) {
+ * // `addresses` is an array of `Address` objects.
+ * });
+ *
+ * //-
+ * // To control how many API requests are made and page through the results
+ * // manually, set `autoPaginate` to `false`.
+ * //-
+ * function callback(err, addresses, nextQuery, apiResponse) {
+ * if (nextQuery) {
+ * // More results exist.
+ * region.getAddresses(nextQuery, callback);
+ * }
+ * }
+ *
+ * region.getAddresses({
+ * autoPaginate: false
+ * }, callback);
+ *
+ * //-
+ * // Get the addresses from your project as a readable object stream.
+ * //-
+ * region.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.
+ * //-
+ * region.getAddresses()
+ * .on('data', function(address) {
+ * this.end();
+ * });
+ */
+Region.prototype.getAddresses = function(options, callback) {
+ var self = this;
+
+ if (is.fn(options)) {
+ callback = options;
+ options = {};
+ }
+
+ options = options || {};
+
+ this.makeReq_('GET', '/addresses', options, null, function(err, resp) {
+ if (err) {
+ callback(err, null, null, resp);
+ return;
+ }
+
+ var nextQuery = null;
+
+ if (resp.nextPageToken) {
+ nextQuery = extend({}, options, {
+ pageToken: resp.nextPageToken
+ });
+ }
+
+ var addresses = (resp.items || []).map(function(address) {
+ var addressInstance = self.address(address.name);
+ addressInstance.metadata = address;
+ return addressInstance;
+ });
+
+ callback(null, addresses, nextQuery, resp);
+ });
+};
+
+/**
+ * Get the region's metadata.
+ *
+ * @resource [Region Resource]{@link https://cloud.google.com/compute/docs/reference/v1/regions}
+ * @resource [Regions: get API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/regions/get}
+ *
+ * @param {function=} callback - The callback function.
+ * @param {?error} callback.err - An error returned while making this request
+ * @param {object} callback.metadata - The region's metadata.
+ * @param {object} callback.apiResponse - The full API response.
+ *
+ * @example
+ * region.getMetadata(function(err, metadata, apiResponse) {});
+ */
+Region.prototype.getMetadata = function(callback) {
+ var self = this;
+
+ callback = callback || util.noop;
+
+ this.makeReq_('GET', '', null, null, function(err, resp) {
+ if (err) {
+ callback(err, null, resp);
+ return;
+ }
+
+ self.metadata = resp;
+
+ callback(null, self.metadata, resp);
+ });
+};
+
+/**
+ * Get a list of operations for this region.
+ *
+ * @resource [Region Operation Overview]{@link https://cloud.google.com/compute/docs/reference/v1/regionOperations}
+ * @resource [RegionOperations: list API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/regionOperations/list}
+ *
+ * @param {object=} options - Operation search options.
+ * @param {boolean} options.autoPaginate - Have pagination handled
+ * automatically. Default: true.
+ * @param {string} options.filter - Search filter in the format of
+ * `{name} {comparison} {filterString}`.
+ * - **`name`**: the name of the field to compare
+ * - **`comparison`**: the comparison operator, `eq` (equal) or `ne`
+ * (not equal)
+ * - **`filterString`**: the string to filter to. For string fields, this
+ * can be a regular expression.
+ * @param {number} options.maxResults - Maximum number of operations to return.
+ * @param {string} options.pageToken - A previously-returned page token
+ * representing part of the larger set of results to view.
+ * @param {function} callback - The callback function.
+ * @param {?error} callback.err - An error returned while making this request.
+ * @param {module:compute/operation} callback.operations - Operation objects
+ * from this region.
+ * @param {?object} callback.nextQuery - If present, query with this object to
+ * check for more results.
+ * @param {object} callback.apiResponse - The full API response.
+ *
+ * @example
+ * region.getOperations(function(err, operations) {
+ * // `operations` is an array of `Operation` objects.
+ * });
+ *
+ * //-
+ * // To control how many API requests are made and page through the results
+ * // manually, set `autoPaginate` to `false`.
+ * //-
+ * function callback(err, operations, nextQuery, apiResponse) {
+ * if (nextQuery) {
+ * // More results exist.
+ * region.getOperations(nextQuery, callback);
+ * }
+ * }
+ *
+ * region.getOperations({
+ * autoPaginate: false
+ * }, callback);
+ *
+ * //-
+ * // Get the operations from your project as a readable object stream.
+ * //-
+ * region.getOperations()
+ * .on('error', console.error)
+ * .on('data', function(operation) {
+ * // `operation` is an `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.
+ * //-
+ * region.getOperations()
+ * .on('data', function(operation) {
+ * this.end();
+ * });
+ */
+Region.prototype.getOperations = function(options, callback) {
+ var self = this;
+
+ if (is.fn(options)) {
+ callback = options;
+ options = {};
+ }
+
+ options = options || {};
+
+ this.makeReq_('GET', '/operations', options, null, function(err, resp) {
+ if (err) {
+ callback(err, null, null, resp);
+ return;
+ }
+
+ var nextQuery = null;
+
+ if (resp.nextPageToken) {
+ nextQuery = extend({}, options, {
+ pageToken: resp.nextPageToken
+ });
+ }
+
+ var operations = (resp.items || []).map(function(operation) {
+ var operationInstance = self.operation(operation.name);
+ operationInstance.metadata = operation;
+ return operationInstance;
+ });
+
+ callback(null, operations, nextQuery, resp);
+ });
+};
+
+/**
+ * Get a reference to a Google Compute Engine region operation.
+ *
+ * @resource [Region Operation Overview]{@link https://cloud.google.com/compute/docs/reference/v1/regionOperations}
+ *
+ * @param {string} name - Name of the existing operation.
+ * @return {module:compute/operation}
+ *
+ * @example
+ * var operation = region.operation('operation-name');
+ */
+Region.prototype.operation = function(name) {
+ return new Operation(this, name);
+};
+
+/**
+ * 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) {
+ path = '/regions/' + this.name + path;
+ 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', 'getOperations']);
+
+module.exports = Region;
diff --git a/lib/compute/snapshot.js b/lib/compute/snapshot.js
new file mode 100644
index 00000000000..256c1192afb
--- /dev/null
+++ b/lib/compute/snapshot.js
@@ -0,0 +1,143 @@
+/*!
+ * Copyright 2015 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/snapshot
+ */
+
+'use strict';
+
+/**
+ * @type {module:common/util}
+ * @private
+ */
+var util = require('../common/util.js');
+
+/*! Developer Documentation
+ *
+ * @param {module:compute} compute - Compute object this snapshot belongs to.
+ * @param {string} name - Snapshot name.
+ */
+/**
+ * A Snapshot object allows you to interact with a Google Compute Engine
+ * snapshot.
+ *
+ * @resource [Snapshots Overview]{@link https://cloud.google.com/compute/docs/disks/persistent-disks#snapshots}
+ * @resource [Snapshot Resource]{@link https://cloud.google.com/compute/docs/reference/v1/snapshots}
+ *
+ * @constructor
+ * @alias module:compute/snapshot
+ *
+ * @example
+ * var gcloud = require('gcloud')({
+ * keyFilename: '/path/to/keyfile.json',
+ * projectId: 'grape-spaceship-123'
+ * });
+ *
+ * var gce = gcloud.compute();
+ *
+ * var snapshot = gce.snapshot('snapshot-name');
+ */
+function Snapshot(compute, name) {
+ this.compute = compute;
+ this.name = name;
+ this.metadata = {};
+}
+
+/**
+ * Delete the snapshot.
+ *
+ * @resource [Snapshots: delete API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/snapshots/delete}
+ *
+ * @param {function=} callback - The callback function.
+ * @param {?error} callback.err - An error returned while making this request.
+ * @param {module:compute/operation} callback.operation - An operation object
+ * that can be used to check the status of the request.
+ * @param {object} callback.apiResponse - The full API response.
+ *
+ * @example
+ * snapshot.delete(function(err, operation, apiResponse) {
+ * // `operation` is an Operation object that can be used to check the status
+ * // of the request.
+ * });
+ */
+Snapshot.prototype.delete = function(callback) {
+ callback = callback || util.noop;
+
+ var compute = this.compute;
+
+ this.makeReq_('DELETE', '', null, null, function(err, resp) {
+ if (err) {
+ callback(err, null, resp);
+ return;
+ }
+
+ var operation = compute.operation(resp.name);
+ operation.metadata = resp;
+
+ callback(null, operation, resp);
+ });
+};
+
+/**
+ * Get the snapshots's metadata.
+ *
+ * @resource [Snapshot Resource]{@link https://cloud.google.com/compute/docs/reference/v1/snapshots}
+ * @resource [Snapshots: get API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/snapshots/get}
+ *
+ * @param {function=} callback - The callback function.
+ * @param {?error} callback.err - An error returned while making this request
+ * @param {object} callback.metadata - The zone's metadata.
+ * @param {object} callback.apiResponse - The full API response.
+ *
+ * @example
+ * snapshot.getMetadata(function(err, metadata, apiResponse) {});
+ */
+Snapshot.prototype.getMetadata = function(callback) {
+ var self = this;
+
+ callback = callback || util.noop;
+
+ this.makeReq_('GET', '', null, null, function(err, resp) {
+ if (err) {
+ callback(err, null, resp);
+ 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;
diff --git a/lib/compute/vm.js b/lib/compute/vm.js
new file mode 100644
index 00000000000..7adfb1732c8
--- /dev/null
+++ b/lib/compute/vm.js
@@ -0,0 +1,411 @@
+/*!
+ * Copyright 2015 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/vm
+ */
+
+'use strict';
+
+var extend = require('extend');
+var is = require('is');
+
+/**
+ * @type {module:compute/disk}
+ * @private
+ */
+var Disk = require('./disk.js');
+
+/**
+ * @type {module:common/util}
+ * @private
+ */
+var util = require('../common/util.js');
+
+/*! Developer Documentation
+ *
+ * @param {module:zone} zone - Zone object this instance belongs to.
+ * @param {string} name - Name of the instance.
+ */
+/**
+ * An Instance object allows you to interact with a Google Compute Engine
+ * instance.
+ *
+ * @resource [Instances and Networks]{@link https://cloud.google.com/compute/docs/instances-and-network}
+ * @resource [Instance Resource]{@link https://cloud.google.com/compute/docs/reference/v1/instances}
+ *
+ * @constructor
+ * @alias module:compute/vm
+ *
+ * @example
+ * var gcloud = require('gcloud')({
+ * keyFilename: '/path/to/keyfile.json',
+ * projectId: 'grape-spaceship-123'
+ * });
+ *
+ * var gce = gcloud.compute();
+ *
+ * var zone = gce.zone('zone-name');
+ *
+ * var vm = zone.vm('vm-name');
+ */
+function VM(zone, name) {
+ this.zone = zone;
+ this.name = name;
+}
+
+/**
+ * Attach a disk to the instance.
+ *
+ * @resource [Disks Overview]{@link https://cloud.google.com/compute/docs/disks}
+ * @resource [Disk Resource]{@link https://cloud.google.com/compute/docs/reference/v1/disks}
+ * @resource [Instance: attachDisk API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/instances/attachDisk}
+ *
+ * @throws {Error} if a {module:compute/disk} is not provided.
+ *
+ * @param {module:compute/disk} disk - The disk to attach.
+ * @param {object=} options - See the
+ * [Instances: attachDisk](https://cloud.google.com/compute/docs/reference/v1/instances/attachDisk)
+ * request body.
+ * @param {boolean} options.readOnly - Attach the disk in read-only mode. (Alias
+ * for `options.mode = READ_ONLY`)
+ * @param {function} callback - The callback function.
+ * @param {?error} callback.err - An error returned while making this request.
+ * @param {module:compute/operation} callback.operation - An operation object
+ * that can be used to check the status of the request.
+ * @param {object} callback.apiResponse - The full API response.
+ *
+ * @example
+ * var disk = zone.disk('my-disk');
+ *
+ * function callback(err, operation, apiResponse) {
+ * // `operation` is an Operation object that can be used to check the status
+ * // of the request.
+ * }
+ *
+ * vm.attachDisk(disk, callback);
+ *
+ * //-
+ * // Provide an options object to customize the request.
+ * //-
+ * var options = {
+ * autoDelete: true,
+ * readOnly: true
+ * };
+ *
+ * vm.attachDisk(disk, options, callback);
+ */
+VM.prototype.attachDisk = function(disk, options, callback) {
+ if (!(disk instanceof Disk)) {
+ throw new Error('A Disk object must be provided.');
+ }
+
+ if (is.fn(options)) {
+ callback = options;
+ options = {};
+ }
+
+ var body = extend({}, options, {
+ source: disk.formattedName
+ });
+
+ if (body.readOnly) {
+ body.mode = 'READ_ONLY';
+ delete body.readOnly;
+ }
+
+ this.makeReq_('POST', '/attachDisk', null, body, callback);
+};
+
+/**
+ * Delete the instance.
+ *
+ * @resource [Instance: delete API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/instances/delete}
+ *
+ * @param {function=} callback - The callback function.
+ * @param {?error} callback.err - An error returned while making this request.
+ * @param {module:compute/operation} callback.operation - An operation object
+ * that can be used to check the status of the request.
+ * @param {object} callback.apiResponse - The full API response.
+ *
+ * @example
+ * vm.delete(function(err, operation, apiResponse) {
+ * // `operation` is an Operation object that can be used to check the status
+ * // of the request.
+ * });
+ */
+VM.prototype.delete = function(callback) {
+ this.makeReq_('DELETE', '', null, null, callback || util.noop);
+};
+
+/**
+ * Detach a disk from the instance.
+ *
+ * @resource [Instance: detachDisk API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/instances/detachDisk}
+ *
+ * @throws {Error} if a {module:compute/disk} is not provided.
+ *
+ * @param {module:compute/disk} disk - The disk to detach.
+ * @param {function=} callback - The callback function.
+ * @param {?error} callback.err - An error returned while making this request.
+ * @param {module:compute/operation} callback.operation - An operation object
+ * that can be used to check the status of the request.
+ * @param {object} callback.apiResponse - The full API response.
+ *
+ * @example
+ * var disk = zone.disk('my-disk');
+ *
+ * vm.detachDisk(disk, function(err, operation, apiResponse) {
+ * // `operation` is an Operation object that can be used to check the status
+ * // of the request.
+ * });
+ */
+VM.prototype.detachDisk = function(disk, callback) {
+ if (!(disk instanceof Disk)) {
+ throw new Error('A Disk object must be provided.');
+ }
+
+ var query = {
+ deviceName: disk.name
+ };
+
+ this.makeReq_('POST', '/detachDisk', query, null, callback || util.noop);
+};
+
+/**
+ * Get the instances's metadata.
+ *
+ * @resource [Instance Resource]{@link https://cloud.google.com/compute/docs/reference/v1/instances}
+ * @resource [Instance: get API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/instances/get}
+ *
+ * @param {function=} callback - The callback function.
+ * @param {?error} callback.err - An error returned while making this request
+ * @param {object} callback.metadata - The instance's metadata.
+ * @param {object} callback.apiResponse - The full API response.
+ *
+ * @example
+ * vm.getMetadata(function(err, metadata, apiResponse) {});
+ */
+VM.prototype.getMetadata = function(callback) {
+ var self = this;
+
+ callback = callback || util.noop;
+
+ this.makeReq_('GET', '', null, null, function(err, _, resp) {
+ if (err) {
+ callback(err, null, resp);
+ return;
+ }
+
+ self.metadata = resp;
+
+ callback(null, self.metadata, resp);
+ });
+};
+
+/**
+ * Returns the serial port output for the instance.
+ *
+ * @resource [Instances: getSerialPortOutput API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/instances/getSerialPortOutput}
+ *
+ * @param {number=} port - The port from which the output is retrieved (1-4).
+ * Default: `1`.
+ * @param {function} callback - The callback function.
+ * @param {?error} callback.err - An error returned while making this request.
+ * @param {object} callback.output - The output from the port.
+ * @param {object} callback.apiResponse - The full API response.
+ *
+ * @example
+ * vm.getSerialPortOutput(function(err, output, apiResponse) {});
+ */
+VM.prototype.getSerialPortOutput = function(port, callback) {
+ if (is.fn(port)) {
+ callback = port;
+ port = 1;
+ }
+
+ var query = {
+ port: port
+ };
+
+ this.makeReq_('GET', '/serialPort', query, null, function(err, _, resp) {
+ if (err) {
+ callback(err, null, resp);
+ return;
+ }
+
+ callback(null, resp.contents, resp);
+ });
+};
+
+/**
+ * Get the instance's tags and their fingerprint.
+ *
+ * This method wraps {module:compute/vm#getMetadata}, returning only the `tags`
+ * property.
+ *
+ * @param {function} callback - The callback function.
+ * @param {?error} callback.err - An error returned while making this request.
+ * @param {object[]} callback.tags - Tag objects from this VM.
+ * @param {string} callback.fingerprint - The current tag fingerprint.
+ * @param {object} callback.apiResponse - The full API response.
+ *
+ * @example
+ * vm.getTags(function(err, tags, fingerprint, apiResponse) {});
+ */
+VM.prototype.getTags = function(callback) {
+ this.getMetadata(function(err, metadata, apiResponse) {
+ if (err) {
+ callback(err, null, null, apiResponse);
+ return;
+ }
+
+ callback(null, metadata.tags.items, metadata.tags.fingerprint, apiResponse);
+ });
+};
+
+/**
+ * Reset the instance.
+ *
+ * @resource [Instances: reset API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/instances/reset}
+ *
+ * @param {function=} callback - The callback function.
+ * @param {?error} callback.err - An error returned while making this request.
+ * @param {module:compute/operation} callback.operation - An operation object
+ * that can be used to check the status of the request.
+ * @param {object} callback.apiResponse - The full API response.
+ *
+ * @example
+ * vm.reset(function(err, operation, apiResponse) {
+ * // `operation` is an Operation object that can be used to check the status
+ * // of the request.
+ * });
+ */
+VM.prototype.reset = function(callback) {
+ this.makeReq_('POST', '/reset', null, null, callback || util.noop);
+};
+
+/**
+ * Set the instance's tags.
+ *
+ * @resource [Instances: setTags API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/instances/setTags}
+ *
+ * @param {string[]} tags - The new tags for the instance.
+ * @param {string} fingerprint - The current tags fingerprint. An up-to-date
+ * fingerprint must be provided.
+ * @param {function=} callback - The callback function.
+ * @param {?error} callback.err - An error returned while making this request.
+ * @param {module:compute/operation} callback.operation - An operation object
+ * that can be used to check the status of the request.
+ * @param {object} callback.apiResponse - The full API response.
+ *
+ * @example
+ * vm.getTags(function(err, tags, fingerprint) {
+ * tags.push('new-tag');
+ *
+ * vm.setTags(tags, fingerprint, function(err, operation, apiResponse) {
+ * // `operation` is an Operation object that can be used to check the
+ * // status of the request.
+ * });
+ * });
+ */
+VM.prototype.setTags = function(tags, fingerprint, callback) {
+ var body = {
+ items: tags,
+ fingerprint: fingerprint
+ };
+
+ this.makeReq_('POST', '/setTags', null, body, callback || util.noop);
+};
+
+/**
+ * Start the instance.
+ *
+ * @resource [Instances: start API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/instances/start}
+ *
+ * @param {function=} callback - The callback function.
+ * @param {?error} callback.err - An error returned while making this request.
+ * @param {module:compute/operation} callback.operation - An operation object
+ * that can be used to check the status of the request.
+ * @param {object} callback.apiResponse - The full API response.
+ *
+ * @example
+ * vm.start(function(err, operation, apiResponse) {
+ * // `operation` is an Operation object that can be used to check the status
+ * // of the request.
+ * });
+ */
+VM.prototype.start = function(callback) {
+ this.makeReq_('POST', '/start', null, null, callback || util.noop);
+};
+
+/**
+ * Stop the instance.
+ *
+ * @resource [Instances: stop API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/instances/stop}
+ *
+ * @param {function=} callback - The callback function.
+ * @param {?error} callback.err - An error returned while making this request.
+ * @param {module:compute/operation} callback.operation - An operation object
+ * that can be used to check the status of the request.
+ * @param {object} callback.apiResponse - The full API response.
+ *
+ * @example
+ * vm.stop(function(err, operation, apiResponse) {
+ * // `operation` is an Operation object that can be used to check the status
+ * // of the request.
+ * });
+ */
+VM.prototype.stop = function(callback) {
+ this.makeReq_('POST', '/stop', null, null, callback || util.noop);
+};
+
+/**
+ * Make a new request object from the provided arguments and wrap the callback
+ * to intercept non-successful responses.
+ *
+ * Most operations on a VM are long-running. This method handles building an
+ * operation and returning it to the user's provided callback. In methods that
+ * don't require an operation, we simply don't do anything with the `Operation`
+ * object.
+ *
+ * @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.
+ */
+VM.prototype.makeReq_ = function(method, path, query, body, callback) {
+ path = '/instances/' + this.name + path;
+
+ var zone = this.zone;
+
+ zone.makeReq_(method, path, query, body, function(err, resp) {
+ if (err) {
+ callback(err, null, resp);
+ return;
+ }
+
+ var operation = zone.operation(resp.name);
+ operation.metadata = resp;
+
+ callback(null, operation, resp);
+ });
+};
+
+module.exports = VM;
diff --git a/lib/compute/zone.js b/lib/compute/zone.js
new file mode 100644
index 00000000000..bb2fe1a782b
--- /dev/null
+++ b/lib/compute/zone.js
@@ -0,0 +1,739 @@
+/*!
+ * Copyright 2015 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';
+
+var extend = require('extend');
+var format = require('string-format-obj');
+var gceImages = require('gce-images');
+var is = require('is');
+
+/**
+ * @type {module:compute/disk}
+ * @private
+ */
+var Disk = require('./disk.js');
+
+/**
+ * @type {module:compute/operation}
+ * @private
+ */
+var Operation = require('./operation.js');
+
+/**
+ * @type {module:common/streamrouter}
+ * @private
+ */
+var streamRouter = require('../common/stream-router.js');
+
+/**
+ * @type {module:common/util}
+ * @private
+ */
+var util = require('../common/util.js');
+
+/**
+ * @type {module:compute/vm}
+ * @private
+ */
+var VM = require('./vm.js');
+
+/*! Developer Documentation
+ *
+ * @param {module:compute} compute - Compute object this zone belongs to.
+ * @param {string} name - Name of the zone.
+ */
+/**
+ * A Zone object allows you to interact with a Google Compute Engine zone.
+ *
+ * @resource [Regions & Zones Overview]{@link https://cloud.google.com/compute/docs/zones}
+ * @resource [Zone Resource]{@link https://cloud.google.com/compute/docs/reference/v1/zones}
+ *
+ * @constructor
+ * @alias module:compute/zone
+ *
+ * @example
+ * var gcloud = require('gcloud')({
+ * keyFilename: '/path/to/keyfile.json',
+ * projectId: 'grape-spaceship-123'
+ * });
+ *
+ * var gce = gcloud.compute();
+ *
+ * var zone = gce.zone('us-central1-a');
+ */
+function Zone(compute, name) {
+ this.compute = compute;
+ this.name = name;
+ this.metadata = {};
+
+ this.gceImages = gceImages(compute.authConfig);
+}
+
+/**
+ * Create a persistent disk in this zone.
+ *
+ * @resource [Disk Resource]{@link https://cloud.google.com/compute/docs/reference/v1/disks}
+ * @resource [Disks: insert API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/disks/insert}
+ *
+ * @param {string} name - Name of the disk.
+ * @param {object} config - See a
+ * [Disk resource](https://cloud.google.com/compute/docs/reference/v1/disks).
+ * @param {string=} config.os - Specify the name of an OS, and we will use the
+ * latest version as the source image of a new boot disk. See
+ * [this list of accepted OS names](https://github.com/stephenplusplus/gce-images#accepted-os-names).
+ * @param {function} callback - The callback function.
+ * @param {?error} callback.err - An error returned while making this request.
+ * @param {module:compute/disk} callback.disk - The created Disk object.
+ * @param {module:compute/operation} callback.operation - An operation object
+ * that can be used to check the status of the request.
+ * @param {object} callback.apiResponse - The full API response.
+ *
+ * @example
+ * var config = {
+ * os: 'ubuntu',
+ * sizeGb: 10
+ * };
+ *
+ * zone.createDisk('name', config, function(err, disk, operation, apiResponse) {
+ * // `disk` is a Disk object.
+ *
+ * // `operation` is an Operation object that can be used to check the status
+ * // of the request.
+ * });
+ */
+Zone.prototype.createDisk = function(name, config, callback) {
+ var self = this;
+
+ var query = {};
+ var body = extend({}, config, {
+ name: name
+ });
+
+ if (body.image) {
+ query.sourceImage = body.image;
+ delete body.image;
+ }
+
+ if (body.os) {
+ this.gceImages.getLatest(body.os, function(err, image) {
+ if (err) {
+ callback(err);
+ return;
+ }
+
+ delete body.os;
+ body.sourceImage = image.selfLink;
+
+ self.createDisk(name, body, callback);
+ });
+ return;
+ }
+
+ this.makeReq_('POST', '/disks', query, body, function(err, resp) {
+ if (err) {
+ callback(err, null, null, resp);
+ return;
+ }
+
+ var disk = self.disk(name);
+
+ var operation = self.operation(resp.name);
+ operation.metadata = resp;
+
+ callback(null, disk, operation, resp);
+ });
+};
+
+/**
+ * Create a virtual machine in this zone.
+ *
+ * @resource [Instance Resource]{@link https://cloud.google.com/compute/docs/reference/v1/instances}
+ * @resource [Instances: insert API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/instances/insert}
+ *
+ * @param {string} name - Name of the instance.
+ * @param {object} config - See an
+ * [Instance resource](https://cloud.google.com/compute/docs/reference/v1/instances).
+ * @param {object[]=} config.disks - See a
+ * [Disk resource](https://cloud.google.com/compute/docs/reference/v1/disks).
+ * @param {boolean=} config.http - Allow HTTP traffic. Default: `false`
+ * @param {boolean=} config.https - Allow HTTPS traffic. Default: `false`
+ * @param {object[]=} config.networkInterfaces - An array of configurations for
+ * this interface. This specifies how this interface should interact with
+ * other network services, such as connecting to the internet. Default:
+ * `[ { network: 'global/networks/default' } ]`
+ * @param {string=} config.machineType - The machine type resource to use.
+ * Provide only the name of the machine, e.g. `n1-standard-16`. Refer to
+ * [Available Machine Types](https://goo.gl/jrHEbo). Default:
+ * `n1-standard-1`
+ * @param {string=} config.os - Specify the name of an OS, and we will use the
+ * latest version as the source image of a new boot disk. See
+ * [this list of accepted OS names](https://github.com/stephenplusplus/gce-images#accepted-os-names).
+ * @param {string[]=} config.tags - An array of tags.
+ * @param {function} callback - The callback function.
+ * @param {?error} callback.err - An error returned while making this request.
+ * @param {module:compute/vm} callback.vm - The created VM object.
+ * @param {module:compute/operation} callback.operation - An operation object
+ * that can be used to check the status of the request.
+ * @param {object} callback.apiResponse - The full API response.
+ *
+ * @example
+ * //-
+ * // Create a new instance using the latest Debian version as the source image
+ * // for a new boot disk.
+ * //-
+ * var config = {
+ * os: 'debian',
+ * http: true,
+ * tags: ['debian-server']
+ * };
+ *
+ * //-
+ * // The above object will auto-expand behind the scenes to something like the
+ * // following. The Debian version may be different when you run the command.
+ * //-
+ * var config = {
+ * machineType: 'n1-standard-1',
+ * disks: [
+ * {
+ * boot: true,
+ * initializeParams: {
+ * sourceImage:
+ * 'https://www.googleapis.com/compute/v1/projects' +
+ * '/debian-cloud/global/images/debian-7-wheezy-v20150710'
+ * }
+ * }
+ * ],
+ * networkInterfaces: [
+ * {
+ * network: 'global/networks/default'
+ * }
+ * ],
+ * tags: [
+ * {
+ * items: [
+ * 'debian-server',
+ * 'http-server'
+ * ]
+ * }
+ * ]
+ * };
+ *
+ * function callback(err, vm, operation, apiResponse) {
+ * // `vm` is a VM object.
+ *
+ * // `operation` is an Operation object that can be used to check the status
+ * // of the request.
+ * }
+ *
+ * zone.createVM('new-vm-name', config, callback);
+ */
+Zone.prototype.createVM = function(name, config, callback) {
+ var self = this;
+
+ var body = extend({
+ name: name,
+ machineType: 'n1-standard-1',
+ networkInterfaces: [
+ {
+ network: 'global/networks/default'
+ }
+ ]
+ }, config);
+
+ if (body.machineType.indexOf('/') === -1) {
+ // The specified machineType is only a partial name, e.g. 'n1-standard-1'.
+ body.machineType = format('zones/{zoneName}/machineTypes/{machineType}', {
+ zoneName: this.name,
+ machineType: body.machineType
+ });
+ }
+
+ if (is.array(body.tags)) {
+ body.tags = {
+ items: body.tags
+ };
+ }
+
+ if (body.http || body.https) {
+ body.networkInterfaces[0].accessConfigs = [
+ {
+ type: 'ONE_TO_ONE_NAT'
+ }
+ ];
+
+ body.tags = body.tags || {};
+ body.tags.items = body.tags.items || [];
+
+ if (body.http) {
+ delete body.http;
+ if (body.tags.items.indexOf('http-server') === -1) {
+ body.tags.items.push('http-server');
+ }
+ }
+
+ if (body.https) {
+ delete body.https;
+ if (body.tags.items.indexOf('https-server') === -1) {
+ body.tags.items.push('https-server');
+ }
+ }
+ }
+
+ if (body.os) {
+ this.gceImages.getLatest(body.os, function(err, image) {
+ if (err) {
+ callback(err);
+ return;
+ }
+
+ delete body.os;
+ body.disks = body.disks || [];
+ body.disks.push({
+ boot: true,
+ initializeParams: {
+ sourceImage: image.selfLink
+ }
+ });
+
+ self.createVM(name, body, callback);
+ });
+ return;
+ }
+
+ this.makeReq_('POST', '/instances', null, body, function(err, resp) {
+ if (err) {
+ callback(err, null, null, resp);
+ return;
+ }
+
+ var vm = self.vm(name);
+
+ var operation = self.operation(resp.name);
+ operation.metadata = resp;
+
+ callback(null, vm, operation, resp);
+ });
+};
+
+/**
+ * Get a reference to a Google Compute Engine disk in this zone.
+ *
+ * @resource [Disks Overview]{@link https://cloud.google.com/compute/docs/disks}
+ *
+ * @param {string} name - Name of the existing disk.
+ * @return {module:compute/disk}
+ *
+ * @example
+ * var disk = zone.disk('disk1');
+ */
+Zone.prototype.disk = function(name) {
+ return new Disk(this, name);
+};
+
+/**
+ * Get a list of disks in this zone.
+ *
+ * @resource [Disks Overview]{@link https://cloud.google.com/compute/docs/disks}
+ * @resource [Disks: list API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/disks/list}
+ *
+ * @param {object=} options - Disk search options.
+ * @param {boolean} options.autoPaginate - Have pagination handled
+ * automatically. Default: true.
+ * @param {string} options.filter - Search filter in the format of
+ * `{name} {comparison} {filterString}`.
+ * - **`name`**: the name of the field to compare
+ * - **`comparison`**: the comparison operator, `eq` (equal) or `ne`
+ * (not equal)
+ * - **`filterString`**: the string to filter to. For string fields, this
+ * can be a regular expression.
+ * @param {number} options.maxResults - Maximum number of disks to return.
+ * @param {string} options.pageToken - A previously-returned page token
+ * representing part of the larger set of results to view.
+ * @param {function} callback - The callback function.
+ * @param {?error} callback.err - An error returned while making this request.
+ * @param {module:compute/disk} callback.disks - Disk objects from this zone.
+ * @param {?object} callback.nextQuery - If present, query with this object to
+ * check for more results.
+ * @param {object} callback.apiResponse - The full API response.
+ *
+ * @example
+ * zone.getDisks(function(err, disks) {
+ * // `disks` is an array of `Disk` objects.
+ * });
+ *
+ * //-
+ * // To control how many API requests are made and page through the results
+ * // manually, set `autoPaginate` to `false`.
+ * //-
+ * function callback(err, disks, nextQuery, apiResponse) {
+ * if (nextQuery) {
+ * // More results exist.
+ * zone.getDisks(nextQuery, callback);
+ * }
+ * }
+ *
+ * zone.getDisks({
+ * autoPaginate: false
+ * }, callback);
+ *
+ * //-
+ * // Get the disks from your project as a readable object stream.
+ * //-
+ * zone.getDisks()
+ * .on('error', console.error)
+ * .on('data', function(disk) {
+ * // `disk` is a `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.
+ * //-
+ * zone.getDisks()
+ * .on('data', function(disk) {
+ * this.end();
+ * });
+ */
+Zone.prototype.getDisks = function(options, callback) {
+ var self = this;
+
+ if (is.fn(options)) {
+ callback = options;
+ options = {};
+ }
+
+ options = options || {};
+
+ this.makeReq_('GET', '/disks', options, null, function(err, resp) {
+ if (err) {
+ callback(err, null, null, resp);
+ return;
+ }
+
+ var nextQuery = null;
+
+ if (resp.nextPageToken) {
+ nextQuery = extend({}, options, {
+ pageToken: resp.nextPageToken
+ });
+ }
+
+ var disks = (resp.items || []).map(function(disk) {
+ var diskInstance = self.disk(disk.name);
+ diskInstance.metadata = disk;
+ return diskInstance;
+ });
+
+ callback(null, disks, nextQuery, resp);
+ });
+};
+
+/**
+ * Get the zone's metadata.
+ *
+ * @resource [Zone Resource]{@link https://cloud.google.com/compute/docs/reference/v1/zones}
+ * @resource [Zones: get API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/zones/get}
+ *
+ * @param {function=} callback - The callback function.
+ * @param {?error} callback.err - An error returned while making this request
+ * @param {object} callback.metadata - The zone's metadata.
+ * @param {object} callback.apiResponse - The full API response.
+ *
+ * @example
+ * zone.getMetadata(function(err, metadata, apiResponse) {});
+ */
+Zone.prototype.getMetadata = function(callback) {
+ var self = this;
+
+ callback = callback || util.noop;
+
+ this.makeReq_('GET', '', null, null, function(err, resp) {
+ if (err) {
+ callback(err, null, resp);
+ return;
+ }
+
+ self.metadata = resp;
+
+ callback(null, self.metadata, resp);
+ });
+};
+
+/**
+ * Get a list of operations for this zone.
+ *
+ * @resource [Zone Operation Overview]{@link https://cloud.google.com/compute/docs/reference/v1/zoneOperations}
+ * @resource [ZoneOperations: list API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/zoneOperations/list}
+ *
+ * @param {object=} options - Operation search options.
+ * @param {boolean} options.autoPaginate - Have pagination handled
+ * automatically. Default: true.
+ * @param {string} options.filter - Search filter in the format of
+ * `{name} {comparison} {filterString}`.
+ * - **`name`**: the name of the field to compare
+ * - **`comparison`**: the comparison operator, `eq` (equal) or `ne`
+ * (not equal)
+ * - **`filterString`**: the string to filter to. For string fields, this
+ * can be a regular expression.
+ * @param {number} options.maxResults - Maximum number of operations to return.
+ * @param {string} options.pageToken - A previously-returned page token
+ * representing part of the larger set of results to view.
+ * @param {function} callback - The callback function.
+ * @param {?error} callback.err - An error returned while making this request.
+ * @param {module:compute/operation} callback.operations - Operation objects
+ * from this zone.
+ * @param {?object} callback.nextQuery - If present, query with this object to
+ * check for more results.
+ * @param {object} callback.apiResponse - The full API response.
+ *
+ * @example
+ * zone.getOperations(function(err, operations) {
+ * // `operations` is an array of `Operation` objects.
+ * });
+ *
+ * //-
+ * // To control how many API requests are made and page through the results
+ * // manually, set `autoPaginate` to `false`.
+ * //-
+ * function callback(err, operations, nextQuery, apiResponse) {
+ * if (nextQuery) {
+ * // More results exist.
+ * zone.getOperations(nextQuery, callback);
+ * }
+ * }
+ *
+ * zone.getOperations({
+ * autoPaginate: false
+ * }, callback);
+ *
+ * //-
+ * // Get the operations from your project as a readable object stream.
+ * //-
+ * zone.getOperations()
+ * .on('error', console.error)
+ * .on('data', function(operation) {
+ * // `operation` is an `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.
+ * //-
+ * zone.getOperations()
+ * .on('data', function(operation) {
+ * this.end();
+ * });
+ */
+Zone.prototype.getOperations = function(options, callback) {
+ var self = this;
+
+ if (is.fn(options)) {
+ callback = options;
+ options = {};
+ }
+
+ options = options || {};
+
+ this.makeReq_('GET', '/operations', options, null, function(err, resp) {
+ if (err) {
+ callback(err, null, null, resp);
+ return;
+ }
+
+ var nextQuery = null;
+
+ if (resp.nextPageToken) {
+ nextQuery = extend({}, options, {
+ pageToken: resp.nextPageToken
+ });
+ }
+
+ var operations = (resp.items || []).map(function(operation) {
+ var operationInstance = self.operation(operation.name);
+ operationInstance.metadata = operation;
+ return operationInstance;
+ });
+
+ callback(null, operations, nextQuery, resp);
+ });
+};
+
+/**
+ * Get a list of VM instances in this zone.
+ *
+ * @resource [Instances and Networks]{@link https://cloud.google.com/compute/docs/instances-and-network}
+ * @resource [Instances: list API Documentation]{@link https://cloud.google.com/compute/docs/reference/v1/instances/list}
+ *
+ * @param {object=} options - Instance search options.
+ * @param {boolean} options.autoPaginate - Have pagination handled
+ * automatically. Default: true.
+ * @param {string} options.filter - Search filter in the format of
+ * `{name} {comparison} {filterString}`.
+ * - **`name`**: the name of the field to compare
+ * - **`comparison`**: the comparison operator, `eq` (equal) or `ne`
+ * (not equal)
+ * - **`filterString`**: the string to filter to. For string fields, this
+ * can be a regular expression.
+ * @param {string} options.pageToken - A previously-returned page token
+ * representing part of the larger set of results to view.
+ * @param {function} callback - The callback function.
+ * @param {?error} callback.err - An error returned while making this request.
+ * @param {module:compute/vm} callback.vms - VM objects from this zone.
+ * @param {?object} callback.nextQuery - If present, query with this object to
+ * check for more results.
+ * @param {object} callback.apiResponse - The full API response.
+ *
+ * @example
+ * zone.getVMs(function(err, vms) {
+ * // `vms` is an array of `VM` objects.
+ * });
+ *
+ * //-
+ * // To control how many API requests are made and page through the results
+ * // manually, set `autoPaginate` to `false`.
+ * //-
+ * function callback(err, vms, nextQuery, apiResponse) {
+ * if (nextQuery) {
+ * // More results exist.
+ * zone.getVMs(nextQuery, callback);
+ * }
+ * }
+ *
+ * zone.getVMs({
+ * autoPaginate: false
+ * }, callback);
+ *
+ * //-
+ * // Get the VM instances from your project as a readable object stream.
+ * //-
+ * zone.getVMs()
+ * .on('error', console.error)
+ * .on('data', function(vm) {
+ * // `vm` is a `VM` 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.
+ * //-
+ * zone.getVMs()
+ * .on('data', function(vm) {
+ * this.end();
+ * });
+ */
+Zone.prototype.getVMs = function(options, callback) {
+ var self = this;
+
+ if (is.fn(options)) {
+ callback = options;
+ options = {};
+ }
+
+ options = options || {};
+
+ this.makeReq_('GET', '/instances', options, null, function(err, resp) {
+ if (err) {
+ callback(err, null, null, resp);
+ return;
+ }
+
+ var nextQuery = null;
+
+ if (resp.nextPageToken) {
+ nextQuery = extend({}, options, {
+ pageToken: resp.nextPageToken
+ });
+ }
+
+ var vms = (resp.items || []).map(function(instance) {
+ var vmInstance = self.vm(instance.name);
+ vmInstance.metadata = instance;
+ return vmInstance;
+ });
+
+ callback(null, vms, nextQuery, resp);
+ });
+};
+
+/**
+ * Get a reference to a Google Compute Engine zone operation.
+ *
+ * @resource [Zone Operation Overview]{@link https://cloud.google.com/compute/docs/reference/v1/zoneOperations}
+ *
+ * @param {string} name - Name of the existing operation.
+ * @return {module:compute/operation}
+ *
+ * @example
+ * var operation = zone.operation('operation-name');
+ */
+Zone.prototype.operation = function(name) {
+ return new Operation(this, name);
+};
+
+/**
+ * Get a reference to a Google Compute Engine virtual machine instance.
+ *
+ * @resource [Instances and Networks]{@link https://cloud.google.com/compute/docs/instances-and-network}
+ *
+ * @param {string} name - Name of the existing virtual machine.
+ * @return {module:compute/vm}
+ *
+ * @example
+ * var vm = zone.vm('vm-name');
+ */
+Zone.prototype.vm = function(name) {
+ return new VM(this, name);
+};
+
+/**
+ * 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) {
+ path = '/zones/' + this.name + path;
+ 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, ['getDisks', 'getOperations', 'getVMs']);
+
+module.exports = Zone;
diff --git a/lib/index.js b/lib/index.js
index 0c2e809316b..a3537d1b511 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -54,6 +54,25 @@ var apis = {
*/
bigquery: require('./bigquery'),
+ /**
+ * With [Compute Engine](https://cloud.google.com/compute/), you can run
+ * large-scale workloads on virtual machines hosted on Google's
+ * infrastructure. Choose a VM that fits your needs and gain the performance
+ * of Google’s worldwide fiber network.
+ *
+ * @type {module:compute}
+ *
+ * @return {module:compute}
+ *
+ * @example
+ * var gcloud = require('gcloud');
+ * var gce = gcloud.compute({
+ * projectId: 'grape-spaceship-123',
+ * keyFilename: '/path/to/keyfile.json'
+ * });
+ */
+ compute: require('./compute'),
+
/**
* [Google Cloud Datastore](https://developers.google.com/datastore/) is a
* fully managed, schemaless database for storing non-relational data. Use
diff --git a/package.json b/package.json
index 4d145b69520..144222faf34 100644
--- a/package.json
+++ b/package.json
@@ -56,6 +56,7 @@
"dns-zonefile": "0.1.9",
"duplexify": "^3.2.0",
"extend": "^2.0.0",
+ "gce-images": "^0.1.0",
"google-auth-library": "^0.9.4",
"is": "^3.0.1",
"methmeth": "^1.0.0",
diff --git a/scripts/docs.sh b/scripts/docs.sh
index 9fa6e898d50..a4f6c6f2f70 100755
--- a/scripts/docs.sh
+++ b/scripts/docs.sh
@@ -21,6 +21,17 @@
./node_modules/.bin/dox < lib/bigquery/job.js > docs/json/master/bigquery/job.json &
./node_modules/.bin/dox < lib/bigquery/table.js > docs/json/master/bigquery/table.json &
+./node_modules/.bin/dox < lib/compute/address.js > docs/json/master/compute/address.json &
+./node_modules/.bin/dox < lib/compute/disk.js > docs/json/master/compute/disk.json &
+./node_modules/.bin/dox < lib/compute/firewall.js > docs/json/master/compute/firewall.json &
+./node_modules/.bin/dox < lib/compute/index.js > docs/json/master/compute/index.json &
+./node_modules/.bin/dox < lib/compute/network.js > docs/json/master/compute/network.json &
+./node_modules/.bin/dox < lib/compute/operation.js > docs/json/master/compute/operation.json &
+./node_modules/.bin/dox < lib/compute/region.js > docs/json/master/compute/region.json &
+./node_modules/.bin/dox < lib/compute/snapshot.js > docs/json/master/compute/snapshot.json &
+./node_modules/.bin/dox < lib/compute/vm.js > docs/json/master/compute/vm.json &
+./node_modules/.bin/dox < lib/compute/zone.js > docs/json/master/compute/zone.json &
+
./node_modules/.bin/dox < lib/dns/change.js > docs/json/master/dns/change.json &
./node_modules/.bin/dox < lib/dns/index.js > docs/json/master/dns/index.json &
./node_modules/.bin/dox < lib/dns/record.js > docs/json/master/dns/record.json &
diff --git a/system-test/compute.js b/system-test/compute.js
new file mode 100644
index 00000000000..edd2ab07031
--- /dev/null
+++ b/system-test/compute.js
@@ -0,0 +1,611 @@
+'use strict';
+
+var assert = require('assert');
+var async = require('async');
+var exec = require('methmeth');
+
+var env = require('./env.js');
+var Compute = require('../lib/compute/index.js');
+
+describe('Compute', function() {
+ // Since the Compute Engine API is rather large and involves long-running
+ // tasks for nearly every request, our system tests follow a pattern designed
+ // to create a minimal amount of resources.
+ //
+ // Each `describe` block tests one object type. Before the tests run, the
+ // object is created.
+ //
+ // The created object is then used and expected to exist for the rest of the
+ // tests in that `describe` block.
+ //
+ // After all describe blocks have run, all of the created objects are
+ // deleted.* This will also pick up any previously-created objects that were
+ // unable to be removed if a prior test run had unexpectedly quit.
+ //
+ // * What we really send are delete requests. If we were to wait on all of the
+ // delete operations to complete, it could be several minutes.
+
+ var TESTS_PREFIX = 'gcloud-tests-';
+ var REGION_NAME = 'us-central1';
+ var ZONE_NAME = 'us-central1-a';
+
+ var compute = new Compute(env);
+ var region = compute.region(REGION_NAME);
+ var zone = compute.zone(ZONE_NAME);
+
+ after(function(done) {
+ deleteAllTestObjects(done);
+ });
+
+ describe('addresses', function() {
+ var ADDRESS_NAME;
+ var address;
+
+ before(function(done) {
+ ADDRESS_NAME = generateName();
+
+ region.createAddress(ADDRESS_NAME, function(err, address_, operation) {
+ assert.ifError(err);
+ address = address_;
+ operation.onComplete(done);
+ });
+ });
+
+ it('should have created the address', function(done) {
+ address.getMetadata(function(err, metadata) {
+ assert.ifError(err);
+ assert.strictEqual(metadata.name, ADDRESS_NAME);
+ done();
+ });
+ });
+
+ it('should get a list of addresses', function(done) {
+ compute.getAddresses(function(err, addresses) {
+ assert.ifError(err);
+ assert(addresses.length > 0);
+ done();
+ });
+ });
+
+ it('should get a list of addresses in stream mode', function(done) {
+ var resultsMatched = 0;
+
+ compute.getAddresses()
+ .on('error', done)
+ .on('data', function() {
+ resultsMatched++;
+ })
+ .on('end', function() {
+ assert(resultsMatched > 0);
+ done();
+ });
+ });
+
+ it('should access an address through a Region', function(done) {
+ region.address(ADDRESS_NAME).getMetadata(done);
+ });
+ });
+
+ describe('disks', function() {
+ var DISK_NAME;
+ var disk;
+
+ before(function(done) {
+ DISK_NAME = generateName();
+
+ var config = {
+ os: 'ubuntu'
+ };
+
+ zone.createDisk(DISK_NAME, config, function(err, disk_, operation) {
+ assert.ifError(err);
+ disk = disk_;
+ operation.onComplete(done);
+ });
+ });
+
+ it('should have created the disk', function(done) {
+ disk.getMetadata(function(err, metadata) {
+ assert.ifError(err);
+ assert.strictEqual(metadata.name, DISK_NAME);
+ done();
+ });
+ });
+
+ it('should get a list of disks', function(done) {
+ compute.getDisks(function(err, disks) {
+ assert.ifError(err);
+ assert(disks.length > 0);
+ done();
+ });
+ });
+
+ it('should get a list of disks in stream mode', function(done) {
+ var resultsMatched = 0;
+
+ compute.getDisks()
+ .on('error', done)
+ .on('data', function() {
+ resultsMatched++;
+ })
+ .on('end', function() {
+ assert(resultsMatched > 0);
+ done();
+ });
+ });
+
+ it('should access a disk through a Zone', function(done) {
+ zone.disk(DISK_NAME).getMetadata(done);
+ });
+
+ it('should take a snapshot', function(done) {
+ disk.createSnapshot(generateName(), done);
+ });
+ });
+
+ describe('firewalls', function() {
+ var FIREWALL_NAME;
+
+ var CONFIG = {
+ protocols: {
+ tcp: [3000],
+ udp: []
+ },
+
+ ranges: ['0.0.0.0/0']
+ };
+
+ var expectedMetadata = {
+ allowed: [
+ {
+ IPProtocol: 'tcp',
+ ports: ['3000']
+ },
+ {
+ IPProtocol: 'udp'
+ }
+ ],
+
+ sourceRanges: CONFIG.ranges
+ };
+
+ var firewall;
+
+ before(function(done) {
+ FIREWALL_NAME = generateName();
+
+ compute.createFirewall(
+ FIREWALL_NAME, CONFIG, function(err, firewall_, operation) {
+ assert.ifError(err);
+ firewall = firewall_;
+ operation.onComplete(done);
+ });
+ });
+
+ it('should have opened the correct connections', function(done) {
+ firewall.getMetadata(function(err, metadata) {
+ assert.ifError(err);
+ assert.deepEqual(metadata.allowed, expectedMetadata.allowed);
+ assert.deepEqual(metadata.sourceRanges, expectedMetadata.sourceRanges);
+ done();
+ });
+ });
+
+ it('should get a list of firewalls', function(done) {
+ compute.getFirewalls(function(err, firewalls) {
+ assert.ifError(err);
+ assert(firewalls.length > 0);
+ done();
+ });
+ });
+
+ it('should get a list of firewalls in stream mode', function(done) {
+ var resultsMatched = 0;
+
+ compute.getFirewalls()
+ .on('error', done)
+ .on('data', function() {
+ resultsMatched++;
+ })
+ .on('end', function() {
+ assert(resultsMatched > 0);
+ done();
+ });
+ });
+ });
+
+ describe('networks', function() {
+ var NETWORK_NAME;
+
+ var CONFIG = {
+ range: '10.240.0.0/16'
+ };
+
+ var network;
+
+ before(function(done) {
+ NETWORK_NAME = generateName();
+
+ compute.createNetwork(
+ NETWORK_NAME, CONFIG, function(err, network_, operation) {
+ assert.ifError(err);
+ network = network_;
+ operation.onComplete(done);
+ });
+ });
+
+ it('should have opened the correct range', function(done) {
+ network.getMetadata(function(err, metadata) {
+ assert.ifError(err);
+ assert.strictEqual(metadata.IPv4Range, CONFIG.range);
+ done();
+ });
+ });
+
+ it('should get a list of networks', function(done) {
+ compute.getNetworks(function(err, networks) {
+ assert.ifError(err);
+ assert(networks.length > 0);
+ done();
+ });
+ });
+
+ it('should get a list of networks in stream mode', function(done) {
+ var resultsMatched = 0;
+
+ compute.getNetworks()
+ .on('error', done)
+ .on('data', function() {
+ resultsMatched++;
+ })
+ .on('end', function() {
+ assert(resultsMatched > 0);
+ done();
+ });
+ });
+ });
+
+ describe('operations', function() {
+ it('should get a list of operations', function(done) {
+ compute.getOperations(function(err, operations) {
+ assert.ifError(err);
+ assert(operations.length > 0);
+ done();
+ });
+ });
+
+ it('should get a list of operations in stream mode', function(done) {
+ var resultsMatched = 0;
+
+ compute.getOperations()
+ .on('error', done)
+ .on('data', function() {
+ resultsMatched++;
+ })
+ .on('end', function() {
+ assert(resultsMatched > 0);
+ done();
+ });
+ });
+ });
+
+ describe('regions', function() {
+ it('should get a list of regions', function(done) {
+ compute.getRegions(function(err, regions) {
+ assert.ifError(err);
+ assert(regions.length > 0);
+ done();
+ });
+ });
+
+ it('should get a list of regions in stream mode', function(done) {
+ var resultsMatched = 0;
+
+ compute.getRegions()
+ .on('error', done)
+ .on('data', function() {
+ resultsMatched++;
+ })
+ .on('end', function() {
+ assert(resultsMatched > 0);
+ done();
+ });
+ });
+
+ it('should get a list of addresses', function(done) {
+ region.getOperations(function(err, addresses) {
+ assert.ifError(err);
+ assert(addresses.length > 0);
+ done();
+ });
+ });
+
+ it('should get a list of addresses in stream mode', function(done) {
+ var resultsMatched = 0;
+
+ region.getOperations()
+ .on('error', done)
+ .on('data', function() {
+ resultsMatched++;
+ })
+ .on('end', function() {
+ assert(resultsMatched > 0);
+ done();
+ });
+ });
+
+ it('should get a list of operations', function(done) {
+ region.getOperations(function(err, operations) {
+ assert.ifError(err);
+ assert(operations.length > 0);
+ done();
+ });
+ });
+
+ it('should get a list of operations in stream mode', function(done) {
+ var resultsMatched = 0;
+
+ region.getOperations()
+ .on('error', done)
+ .on('data', function() {
+ resultsMatched++;
+ })
+ .on('end', function() {
+ assert(resultsMatched > 0);
+ done();
+ });
+ });
+ });
+
+ describe('snapshots', function() {
+ it('should get a list of snapshots', function(done) {
+ compute.getSnapshots(function(err, snapshots) {
+ assert.ifError(err);
+ assert(snapshots.length > 0);
+ done();
+ });
+ });
+
+ it('should get a list of snapshots in stream mode', function(done) {
+ var resultsMatched = 0;
+
+ compute.getSnapshots()
+ .on('error', done)
+ .on('data', function() {
+ resultsMatched++;
+ })
+ .on('end', function() {
+ assert(resultsMatched > 0);
+ done();
+ });
+ });
+ });
+
+ describe('vms', function() {
+ var VM_NAME;
+ var vm;
+
+ before(function(done) {
+ VM_NAME = generateName();
+
+ var config = {
+ os: 'ubuntu',
+ http: true
+ };
+
+ zone.createVM(VM_NAME, config, function(err, vm_, operation) {
+ assert.ifError(err);
+ vm = vm_;
+ operation.onComplete(done);
+ });
+ });
+
+ after(function(done) {
+ // 90s is the minimum time for this operation to complete as per:
+ // https://cloud.google.com/compute/docs/instances/#deleting_an_instance
+ //
+ // In practice, it seems to take around 1.5x that, so we allow 2x.
+ var MAX_TIME_ALLOWED = 90000 * 2;
+ this.timeout(MAX_TIME_ALLOWED);
+
+ vm.delete(function(err, operation) {
+ if (err) {
+ done(err);
+ return;
+ }
+
+ operation.onComplete({
+ maxAttempts: MAX_TIME_ALLOWED / 10000,
+ interval: 10000
+ }, done);
+ });
+ });
+
+ it('should have enabled HTTP connections', function(done) {
+ vm.getTags(function(err, tags) {
+ assert.ifError(err);
+ assert.deepEqual(tags, ['http-server']);
+ done();
+ });
+ });
+
+ it('should get a list of vms', function(done) {
+ compute.getVMs(function(err, vms) {
+ assert.ifError(err);
+ assert(vms.length > 0);
+ done();
+ });
+ });
+
+ it('should get a list of vms in stream mode', function(done) {
+ var resultsMatched = 0;
+
+ compute.getVMs()
+ .on('error', done)
+ .on('data', function() {
+ resultsMatched++;
+ })
+ .on('end', function() {
+ assert(resultsMatched > 0);
+ done();
+ });
+ });
+
+ it('should access a VM through a Zone', function(done) {
+ zone.vm(VM_NAME).getMetadata(done);
+ });
+
+ it('should attach and detach a disk', function(done) {
+ compute.getDisks()
+ .on('error', done)
+ .once('data', function(disk) {
+ this.end();
+
+ vm.attachDisk(disk, function(err) {
+ assert.ifError(err);
+
+ vm.detachDisk(disk, function(err, operation) {
+ assert.ifError(err);
+ operation.onComplete(done);
+ });
+ });
+ });
+ });
+
+ it('should get serial port output', function(done) {
+ vm.getSerialPortOutput(done);
+ });
+
+ it('should set tags', function(done) {
+ var newTagName = 'new-tag';
+
+ vm.getTags(function(err, tags, fingerprint) {
+ assert.ifError(err);
+
+ tags.push(newTagName);
+
+ vm.setTags(tags, fingerprint, function(err, operation) {
+ assert.ifError(err);
+
+ operation.onComplete(function(err) {
+ assert.ifError(err);
+
+ vm.getTags(function(err, tags) {
+ assert.ifError(err);
+ assert(tags.indexOf(newTagName) > -1);
+ done();
+ });
+ });
+ });
+ });
+ });
+
+ it('should reset', function(done) {
+ vm.reset(done);
+ });
+
+ it('should start', function(done) {
+ vm.start(done);
+ });
+
+ it('should stop', function(done) {
+ vm.stop(done);
+ });
+ });
+
+ describe('zones', function() {
+ it('should get a list of zones', function(done) {
+ compute.getZones(function(err, zones) {
+ assert.ifError(err);
+ assert(zones.length > 0);
+ done();
+ });
+ });
+
+ it('should get a list of zones in stream mode', function(done) {
+ var resultsMatched = 0;
+
+ compute.getZones()
+ .on('error', done)
+ .on('data', function() {
+ resultsMatched++;
+ })
+ .on('end', function() {
+ assert(resultsMatched > 0);
+ done();
+ });
+ });
+
+ it('should get a list of disks', function(done) {
+ zone.getDisks(function(err, disks) {
+ assert.ifError(err);
+ assert(disks.length > 0);
+ done();
+ });
+ });
+
+ it('should get a list of disks in stream mode', function(done) {
+ var resultsMatched = 0;
+
+ zone.getDisks()
+ .on('error', done)
+ .on('data', function() {
+ resultsMatched++;
+ })
+ .on('end', function() {
+ assert(resultsMatched > 0);
+ done();
+ });
+ });
+
+ it('should get a list of operations', function(done) {
+ zone.getOperations(function(err, operations) {
+ assert.ifError(err);
+ assert(operations.length > 0);
+ done();
+ });
+ });
+
+ it('should get a list of operations in stream mode', function(done) {
+ var resultsMatched = 0;
+
+ zone.getOperations()
+ .on('error', done)
+ .on('data', function() {
+ resultsMatched++;
+ })
+ .on('end', function() {
+ assert(resultsMatched > 0);
+ done();
+ });
+ });
+ });
+
+ function generateName() {
+ return TESTS_PREFIX + Date.now();
+ }
+
+ function deleteAllTestObjects(callback) {
+ async.each([
+ 'getAddresses',
+ 'getDisks',
+ 'getFirewalls',
+ 'getNetworks',
+ 'getSnapshots',
+ 'getVMs'
+ ], callAndDelete, callback);
+ }
+
+ function callAndDelete(methodName, callback) {
+ compute[methodName]({
+ filter: 'name eq ' + TESTS_PREFIX + '.*'
+ }, function(err, objects) {
+ if (err) {
+ callback(err);
+ return;
+ }
+
+ async.each(objects, exec('delete'), callback);
+ });
+ }
+});
diff --git a/test/compute/address.js b/test/compute/address.js
new file mode 100644
index 00000000000..17d6bbb8d4f
--- /dev/null
+++ b/test/compute/address.js
@@ -0,0 +1,221 @@
+/**
+ * Copyright 2015 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.
+ */
+
+'use strict';
+
+var assert = require('assert');
+var Address = require('../../lib/compute/address');
+
+describe('Address', function() {
+ var address;
+
+ var ADDRESS_NAME = 'us-central1';
+ var REGION = {};
+
+ beforeEach(function() {
+ address = new Address(REGION, ADDRESS_NAME);
+ });
+
+ describe('instantiation', function() {
+ it('should localize the region', function() {
+ assert.strictEqual(address.region, REGION);
+ });
+
+ it('should localize the name', function() {
+ assert.strictEqual(address.name, ADDRESS_NAME);
+ });
+
+ it('should default metadata to an empty object', function() {
+ assert.strictEqual(typeof address.metadata, 'object');
+ assert.strictEqual(Object.keys(address.metadata).length, 0);
+ });
+ });
+
+ describe('delete', function() {
+ it('should make the correct API request', function(done) {
+ address.makeReq_ = function(method, path, query, body) {
+ assert.strictEqual(method, 'DELETE');
+ assert.strictEqual(path, '');
+ assert.strictEqual(query, null);
+ assert.strictEqual(body, null);
+ done();
+ };
+
+ address.delete(assert.ifError);
+ });
+
+ describe('error', function() {
+ var error = new Error('Error.');
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ address.makeReq_ = function(method, path, query, body, callback) {
+ callback(error, apiResponse);
+ };
+ });
+
+ it('should return an error if the request fails', function(done) {
+ address.delete(function(err, operation, apiResponse_) {
+ assert.strictEqual(err, error);
+ assert.strictEqual(operation, null);
+ assert.strictEqual(apiResponse_, apiResponse);
+ done();
+ });
+ });
+
+ it('should not require a callback', function() {
+ assert.doesNotThrow(function() {
+ address.delete();
+ });
+ });
+ });
+
+ describe('success', function() {
+ var apiResponse = {
+ name: 'op-name'
+ };
+
+ beforeEach(function() {
+ address.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponse);
+ };
+ });
+
+ it('should execute callback with Operation & Response', function(done) {
+ var operation = {};
+
+ address.region.operation = function(name) {
+ assert.strictEqual(name, apiResponse.name);
+ return operation;
+ };
+
+ address.delete(function(err, operation_, apiResponse_) {
+ assert.ifError(err);
+ assert.strictEqual(operation_, operation);
+ assert.strictEqual(apiResponse_, apiResponse);
+ done();
+ });
+ });
+
+ it('should not require a callback', function() {
+ assert.doesNotThrow(function() {
+ address.delete();
+ });
+ });
+ });
+ });
+
+ describe('getMetadata', function() {
+ it('should make the correct API request', function(done) {
+ address.makeReq_ = function(method, path, query, body) {
+ assert.strictEqual(method, 'GET');
+ assert.strictEqual(path, '');
+ assert.strictEqual(query, null);
+ assert.strictEqual(body, null);
+
+ done();
+ };
+
+ address.getMetadata(assert.ifError);
+ });
+
+ describe('error', function() {
+ var error = new Error('Error.');
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ address.makeReq_ = function(method, path, query, body, callback) {
+ callback(error, apiResponse);
+ };
+ });
+
+ it('should execute callback with error and API response', function(done) {
+ address.getMetadata(function(err, metadata, apiResponse_) {
+ assert.strictEqual(err, error);
+ assert.strictEqual(metadata, null);
+ assert.strictEqual(apiResponse_, apiResponse);
+ done();
+ });
+ });
+
+ it('should not require a callback', function() {
+ assert.doesNotThrow(function() {
+ address.getMetadata();
+ });
+ });
+ });
+
+ describe('success', function() {
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ address.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponse);
+ };
+ });
+
+ it('should update the metadata to the API response', function(done) {
+ address.getMetadata(function(err) {
+ assert.ifError(err);
+ assert.strictEqual(address.metadata, apiResponse);
+ done();
+ });
+ });
+
+ it('should exec callback with metadata and API response', function(done) {
+ address.getMetadata(function(err, metadata, apiResponse_) {
+ assert.ifError(err);
+ assert.strictEqual(metadata, apiResponse);
+ assert.strictEqual(apiResponse_, apiResponse);
+ done();
+ });
+ });
+
+ it('should not require a callback', function() {
+ assert.doesNotThrow(function() {
+ address.getMetadata();
+ });
+ });
+ });
+ });
+
+ describe('makeReq_', function() {
+ it('should make the correct request to Compute', function(done) {
+ var expectedPathPrefix = '/addresses/' + address.name;
+
+ var method = 'POST';
+ var path = '/test';
+ var query = {
+ a: 'b',
+ c: 'd'
+ };
+ var body = {
+ a: 'b',
+ c: 'd'
+ };
+
+ address.region.makeReq_ = function(method_, path_, query_, body_, cb) {
+ assert.strictEqual(method_, method);
+ assert.strictEqual(path_, expectedPathPrefix + path);
+ assert.strictEqual(query_, query);
+ assert.strictEqual(body_, body);
+ cb();
+ };
+
+ address.makeReq_(method, path, query, body, done);
+ });
+ });
+});
diff --git a/test/compute/disk.js b/test/compute/disk.js
new file mode 100644
index 00000000000..27486da5361
--- /dev/null
+++ b/test/compute/disk.js
@@ -0,0 +1,340 @@
+/**
+ * Copyright 2015 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.
+ */
+
+'use strict';
+
+var assert = require('assert');
+var format = require('string-format-obj');
+
+var Disk = require('../../lib/compute/disk');
+var util = require('../../lib/common/util');
+
+describe('Disk', function() {
+ var disk;
+
+ var COMPUTE = { projectId: 'project-id' };
+ var ZONE = { compute: COMPUTE, name: 'us-central1-a' };
+ var DISK_NAME = 'disk-name';
+ var DISK_FULL_NAME = format('projects/{pId}/zones/{zName}/disks/{dName}', {
+ pId: COMPUTE.projectId,
+ zName: ZONE.name,
+ dName: DISK_NAME
+ });
+
+ beforeEach(function() {
+ disk = new Disk(ZONE, DISK_NAME);
+ });
+
+ describe('instantiation', function() {
+ it('should localize the zone', function() {
+ assert.strictEqual(disk.zone, ZONE);
+ });
+
+ it('should localize the name', function() {
+ assert.strictEqual(disk.name, DISK_NAME);
+ });
+
+ it('should default metadata to an empty object', function() {
+ assert.strictEqual(typeof disk.metadata, 'object');
+ assert.strictEqual(Object.keys(disk.metadata).length, 0);
+ });
+
+ it('should format the disk name', function() {
+ var formatName_ = Disk.formatName_;
+ var formattedName = 'projects/a/zones/b/disks/c';
+
+ Disk.formatName_ = function(zone, name) {
+ Disk.formatName_ = formatName_;
+
+ assert.strictEqual(zone, ZONE);
+ assert.strictEqual(name, DISK_NAME);
+
+ return formattedName;
+ };
+
+ var disk = new Disk(ZONE, DISK_NAME);
+ assert(disk.formattedName, formattedName);
+ });
+ });
+
+ describe('formatName_', function() {
+ it('should format the name', function() {
+ var formattedName_ = Disk.formatName_(ZONE, DISK_NAME);
+ assert.strictEqual(formattedName_, DISK_FULL_NAME);
+ });
+ });
+
+ describe('createSnapshot', function() {
+ it('should make the correct API request', function(done) {
+ disk.makeReq_ = function(method, path, query, body) {
+ assert.strictEqual(method, 'POST');
+ assert.strictEqual(path, '/createSnapshot');
+ assert.strictEqual(query, null);
+ assert.deepEqual(body, { name: 'test', a: 'b' });
+ done();
+ };
+
+ disk.createSnapshot('test', { a: 'b' }, util.noop);
+ });
+
+ describe('error', function() {
+ var error = new Error('Error.');
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ disk.makeReq_ = function(method, path, query, body, callback) {
+ callback(error, apiResponse);
+ };
+ });
+
+ it('should return an error if the request fails', function(done) {
+ disk.createSnapshot('test', {}, function(err, snap, op, apiResponse_) {
+ assert.strictEqual(err, error);
+ assert.strictEqual(snap, null);
+ assert.strictEqual(op, null);
+ assert.strictEqual(apiResponse_, apiResponse);
+ done();
+ });
+ });
+
+ it('should not require options', function() {
+ assert.doesNotThrow(function() {
+ disk.createSnapshot('test', util.noop);
+ });
+ });
+ });
+
+ describe('success', function() {
+ var apiResponse = {
+ name: 'op-name'
+ };
+
+ beforeEach(function() {
+ disk.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponse);
+ };
+ });
+
+ it('should execute callback with Snapshot & Operation', function(done) {
+ var snapshot = {};
+ var operation = {};
+
+ disk.zone.compute.snapshot = function(name) {
+ assert.strictEqual(name, 'test');
+ return snapshot;
+ };
+
+ disk.zone.operation = function(name) {
+ assert.strictEqual(name, apiResponse.name);
+ return operation;
+ };
+
+ disk.createSnapshot('test', {}, function(err, snap, op, apiResponse_) {
+ assert.ifError(err);
+
+ assert.strictEqual(snap, snapshot);
+ assert.strictEqual(op, operation);
+ assert.strictEqual(op.metadata, apiResponse);
+ assert.strictEqual(apiResponse_, apiResponse);
+
+ done();
+ });
+
+ it('should not require options', function() {
+ assert.doesNotThrow(function() {
+ disk.createSnapshot('test', util.noop);
+ });
+ });
+ });
+ });
+ });
+
+ describe('delete', function() {
+ it('should make the correct API request', function(done) {
+ disk.makeReq_ = function(method, path, query, body) {
+ assert.strictEqual(method, 'DELETE');
+ assert.strictEqual(path, '');
+ assert.strictEqual(query, null);
+ assert.strictEqual(body, null);
+ done();
+ };
+
+ disk.delete(assert.ifError);
+ });
+
+ describe('error', function() {
+ var error = new Error('Error.');
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ disk.makeReq_ = function(method, path, query, body, callback) {
+ callback(error, apiResponse);
+ };
+ });
+
+ it('should return an error if the request fails', function(done) {
+ disk.delete(function(err, operation, apiResponse_) {
+ assert.strictEqual(err, error);
+ assert.strictEqual(operation, null);
+ assert.strictEqual(apiResponse_, apiResponse);
+ done();
+ });
+ });
+
+ it('should not require a callback', function() {
+ assert.doesNotThrow(function() {
+ disk.delete();
+ });
+ });
+ });
+
+ describe('success', function() {
+ var apiResponse = {
+ name: 'op-name'
+ };
+
+ beforeEach(function() {
+ disk.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponse);
+ };
+ });
+
+ it('should execute callback with Operation & Response', function(done) {
+ var operation = {};
+
+ disk.zone.operation = function(name) {
+ assert.strictEqual(name, apiResponse.name);
+ return operation;
+ };
+
+ disk.delete(function(err, operation_, apiResponse_) {
+ assert.ifError(err);
+ assert.strictEqual(operation_, operation);
+ assert.strictEqual(operation_.metadata, apiResponse);
+ assert.strictEqual(apiResponse_, apiResponse);
+ done();
+ });
+ });
+
+ it('should not require a callback', function() {
+ assert.doesNotThrow(function() {
+ disk.delete();
+ });
+ });
+ });
+ });
+
+ describe('getMetadata', function() {
+ it('should make the correct API request', function(done) {
+ disk.makeReq_ = function(method, path, query, body) {
+ assert.strictEqual(method, 'GET');
+ assert.strictEqual(path, '');
+ assert.strictEqual(query, null);
+ assert.strictEqual(body, null);
+
+ done();
+ };
+
+ disk.getMetadata(assert.ifError);
+ });
+
+ describe('error', function() {
+ var error = new Error('Error.');
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ disk.makeReq_ = function(method, path, query, body, callback) {
+ callback(error, apiResponse);
+ };
+ });
+
+ it('should execute callback with error and API response', function(done) {
+ disk.getMetadata(function(err, metadata, apiResponse_) {
+ assert.strictEqual(err, error);
+ assert.strictEqual(metadata, null);
+ assert.strictEqual(apiResponse_, apiResponse);
+ done();
+ });
+ });
+
+ it('should not require a callback', function() {
+ assert.doesNotThrow(function() {
+ disk.getMetadata();
+ });
+ });
+ });
+
+ describe('success', function() {
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ disk.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponse);
+ };
+ });
+
+ it('should update the metadata to the API response', function(done) {
+ disk.getMetadata(function(err) {
+ assert.ifError(err);
+ assert.strictEqual(disk.metadata, apiResponse);
+ done();
+ });
+ });
+
+ it('should exec callback with metadata and API response', function(done) {
+ disk.getMetadata(function(err, metadata, apiResponse_) {
+ assert.ifError(err);
+ assert.strictEqual(metadata, apiResponse);
+ assert.strictEqual(apiResponse_, apiResponse);
+ done();
+ });
+ });
+
+ it('should not require a callback', function() {
+ assert.doesNotThrow(function() {
+ disk.getMetadata();
+ });
+ });
+ });
+ });
+
+ describe('makeReq_', function() {
+ it('should make the correct request to Compute', function(done) {
+ var expectedPathPrefix = '/disks/' + disk.name;
+
+ var method = 'POST';
+ var path = '/test';
+ var query = {
+ a: 'b',
+ c: 'd'
+ };
+ var body = {
+ a: 'b',
+ c: 'd'
+ };
+
+ disk.zone.makeReq_ = function(method_, path_, query_, body_, cb) {
+ assert.strictEqual(method_, method);
+ assert.strictEqual(path_, expectedPathPrefix + path);
+ assert.strictEqual(query_, query);
+ assert.strictEqual(body_, body);
+ cb();
+ };
+
+ disk.makeReq_(method, path, query, body, done);
+ });
+ });
+});
diff --git a/test/compute/firewall.js b/test/compute/firewall.js
new file mode 100644
index 00000000000..f6a8b4c776d
--- /dev/null
+++ b/test/compute/firewall.js
@@ -0,0 +1,296 @@
+/**
+ * Copyright 2015 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.
+ */
+
+'use strict';
+
+var assert = require('assert');
+var Firewall = require('../../lib/compute/firewall');
+
+describe('Firewall', function() {
+ var firewall;
+
+ var COMPUTE = { projectId: 'project-id' };
+ var FIREWALL_NAME = 'tcp-3000';
+ var FIREWALL_NETWORK = 'global/networks/default';
+
+ beforeEach(function() {
+ firewall = new Firewall(COMPUTE, FIREWALL_NAME);
+ });
+
+ describe('instantiation', function() {
+ it('should localize compute instance', function() {
+ assert.strictEqual(firewall.compute, COMPUTE);
+ });
+
+ it('should localize the firewall name', function() {
+ assert.strictEqual(firewall.name, FIREWALL_NAME);
+ });
+
+ it('should create default metadata', function() {
+ assert.deepEqual(firewall.metadata, { network: FIREWALL_NETWORK });
+ });
+ });
+
+ describe('delete', function() {
+ it('should make the correct API request', function(done) {
+ firewall.makeReq_ = function(method, path, query, body) {
+ assert.strictEqual(method, 'DELETE');
+ assert.strictEqual(path, '');
+ assert.strictEqual(query, null);
+ assert.strictEqual(body, null);
+ done();
+ };
+
+ firewall.delete(assert.ifError);
+ });
+
+ describe('error', function() {
+ var error = new Error('Error.');
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ firewall.makeReq_ = function(method, path, query, body, callback) {
+ callback(error, apiResponse);
+ };
+ });
+
+ it('should return an error if the request fails', function(done) {
+ firewall.delete(function(err, operation, apiResponse_) {
+ assert.strictEqual(err, error);
+ assert.strictEqual(operation, null);
+ assert.strictEqual(apiResponse_, apiResponse);
+ done();
+ });
+ });
+
+ it('should not require a callback', function() {
+ assert.doesNotThrow(function() {
+ firewall.delete();
+ });
+ });
+ });
+
+ describe('success', function() {
+ var apiResponse = {
+ name: 'op-name'
+ };
+
+ beforeEach(function() {
+ firewall.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponse);
+ };
+ });
+
+ it('should execute callback with Operation & Response', function(done) {
+ var operation = {};
+
+ firewall.compute.operation = function(name) {
+ assert.strictEqual(name, apiResponse.name);
+ return operation;
+ };
+
+ firewall.delete(function(err, operation_, apiResponse_) {
+ assert.ifError(err);
+ assert.strictEqual(operation_, operation);
+ assert.strictEqual(apiResponse_, apiResponse);
+ done();
+ });
+ });
+
+ it('should not require a callback', function() {
+ assert.doesNotThrow(function() {
+ firewall.delete();
+ });
+ });
+ });
+ });
+
+ describe('getMetadata', function() {
+ it('should make the correct API request', function(done) {
+ firewall.makeReq_ = function(method, path, query, body) {
+ assert.strictEqual(method, 'GET');
+ assert.strictEqual(path, '');
+ assert.strictEqual(query, null);
+ assert.strictEqual(body, null);
+
+ done();
+ };
+
+ firewall.getMetadata(assert.ifError);
+ });
+
+ describe('error', function() {
+ var error = new Error('Error.');
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ firewall.makeReq_ = function(method, path, query, body, callback) {
+ callback(error, apiResponse);
+ };
+ });
+
+ it('should execute callback with error and API response', function(done) {
+ firewall.getMetadata(function(err, metadata, apiResponse_) {
+ assert.strictEqual(err, error);
+ assert.strictEqual(metadata, null);
+ assert.strictEqual(apiResponse_, apiResponse);
+ done();
+ });
+ });
+
+ it('should not require a callback', function() {
+ assert.doesNotThrow(function() {
+ firewall.getMetadata();
+ });
+ });
+ });
+
+ describe('success', function() {
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ firewall.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponse);
+ };
+ });
+
+ it('should update the metadata to the API response', function(done) {
+ firewall.getMetadata(function(err) {
+ assert.ifError(err);
+ assert.strictEqual(firewall.metadata, apiResponse);
+ done();
+ });
+ });
+
+ it('should exec callback with metadata and API response', function(done) {
+ firewall.getMetadata(function(err, metadata, apiResponse_) {
+ assert.ifError(err);
+ assert.strictEqual(metadata, apiResponse);
+ assert.strictEqual(apiResponse_, apiResponse);
+ done();
+ });
+ });
+
+ it('should not require a callback', function() {
+ assert.doesNotThrow(function() {
+ firewall.getMetadata();
+ });
+ });
+ });
+ });
+
+ describe('setMetadata', function() {
+ it('should make the correct API request', function(done) {
+ firewall.makeReq_ = function(method, path, query, body) {
+ assert.strictEqual(method, 'PATCH');
+ assert.strictEqual(path, '');
+ assert.strictEqual(query, null);
+ assert.deepEqual(body, {
+ name: FIREWALL_NAME,
+ network: FIREWALL_NETWORK,
+ a: 'b'
+ });
+
+ done();
+ };
+
+ firewall.setMetadata({ a: 'b' }, assert.ifError);
+ });
+
+ describe('error', function() {
+ var error = new Error('Error.');
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ firewall.makeReq_ = function(method, path, query, body, callback) {
+ callback(error, apiResponse);
+ };
+ });
+
+ it('should return an error if the request fails', function(done) {
+ firewall.setMetadata({ e: 'f' }, function(err, op, apiResponse_) {
+ assert.strictEqual(err, error);
+ assert.strictEqual(op, null);
+ assert.strictEqual(apiResponse_, apiResponse);
+ done();
+ });
+ });
+ });
+
+ describe('success', function() {
+ var apiResponse = {
+ name: 'op-name'
+ };
+
+ beforeEach(function() {
+ firewall.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponse);
+ };
+ });
+
+ it('should execute callback with operation & response', function(done) {
+ var operation = {};
+ var metadata = { a: 'b' };
+
+ firewall.compute.operation = function(name) {
+ assert.strictEqual(name, apiResponse.name);
+ return operation;
+ };
+
+ firewall.setMetadata(metadata, function(err, op, apiResponse_) {
+ assert.ifError(err);
+ assert.strictEqual(op, operation);
+ assert.strictEqual(op.metadata, apiResponse);
+ assert.strictEqual(apiResponse_, apiResponse);
+ done();
+ });
+ });
+
+ it('should not require a callback', function() {
+ assert.doesNotThrow(function() {
+ firewall.setMetadata({ a: 'b' });
+ });
+ });
+ });
+ });
+
+ describe('makeReq_', function() {
+ it('should make the correct request to Compute', function(done) {
+ var expectedPathPrefix = '/global/firewalls/' + firewall.name;
+
+ var method = 'POST';
+ var path = '/test';
+ var query = {
+ a: 'b',
+ c: 'd'
+ };
+ var body = {
+ a: 'b',
+ c: 'd'
+ };
+
+ firewall.compute.makeReq_ = function(method_, path_, query_, body_, cb) {
+ assert.strictEqual(method_, method);
+ assert.strictEqual(path_, expectedPathPrefix + path);
+ assert.strictEqual(query_, query);
+ assert.strictEqual(body_, body);
+ cb();
+ };
+
+ firewall.makeReq_(method, path, query, body, done);
+ });
+ });
+});
diff --git a/test/compute/index.js b/test/compute/index.js
new file mode 100644
index 00000000000..31937060d00
--- /dev/null
+++ b/test/compute/index.js
@@ -0,0 +1,1403 @@
+/**
+ * Copyright 2015 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.
+ */
+
+'use strict';
+
+var assert = require('assert');
+var mockery = require('mockery');
+var extend = require('extend');
+var arrify = require('arrify');
+
+var util = require('../../lib/common/util.js');
+var slice = Array.prototype.slice;
+
+var fakeUtil = extend({}, util, {
+ makeAuthorizedRequestFactory: util.noop
+});
+
+var extended = false;
+var fakeStreamRouter = {
+ extend: function(Class, methods) {
+ if (Class.name !== 'Compute') {
+ return;
+ }
+
+ extended = true;
+ methods = arrify(methods);
+ assert.deepEqual(methods, [
+ 'getAddresses',
+ 'getDisks',
+ 'getFirewalls',
+ 'getNetworks',
+ 'getOperations',
+ 'getRegions',
+ 'getSnapshots',
+ 'getVMs',
+ 'getZones'
+ ]);
+ }
+};
+
+function FakeFirewall() {
+ this.calledWith_ = slice.call(arguments);
+}
+
+function FakeNetwork() {
+ this.calledWith_ = slice.call(arguments);
+}
+
+function FakeOperation() {
+ this.calledWith_ = slice.call(arguments);
+}
+
+function FakeRegion() {
+ this.calledWith_ = slice.call(arguments);
+ this.address = function() { return {}; };
+}
+
+function FakeSnapshot() {
+ this.calledWith_ = slice.call(arguments);
+}
+
+function FakeZone() {
+ this.calledWith_ = slice.call(arguments);
+ this.disk = function() { return {}; };
+ this.vm = function() { return {}; };
+}
+
+describe('Compute', function() {
+ var Compute;
+ var compute;
+
+ var PROJECT_ID = 'project-id';
+
+ before(function() {
+ mockery.registerMock('../common/util.js', fakeUtil);
+ mockery.registerMock('../common/stream-router.js', fakeStreamRouter);
+ mockery.registerMock('./firewall.js', FakeFirewall);
+ mockery.registerMock('./network.js', FakeNetwork);
+ mockery.registerMock('./operation.js', FakeOperation);
+ mockery.registerMock('./region.js', FakeRegion);
+ mockery.registerMock('./snapshot.js', FakeSnapshot);
+ mockery.registerMock('./zone.js', FakeZone);
+
+ mockery.enable({
+ useCleanCache: true,
+ warnOnUnregistered: false
+ });
+
+ Compute = require('../../lib/compute');
+ });
+
+ after(function() {
+ mockery.deregisterAll();
+ mockery.disable();
+ });
+
+ beforeEach(function() {
+ compute = new Compute({
+ projectId: PROJECT_ID
+ });
+ });
+
+ describe('instantiation', function() {
+ var options = {
+ projectId: PROJECT_ID,
+ credentials: 'credentials',
+ email: 'email',
+ keyFilename: 'keyFile'
+ };
+
+ it('should return a new instance of Compute', function() {
+ var compute = new Compute({ projectId: PROJECT_ID });
+ assert(compute instanceof Compute);
+ });
+
+ it('should throw if projectId is not provided', function() {
+ assert.throws(Compute, new RegExp(util.missingProjectIdError));
+ });
+
+ it('should localize authConfig', function() {
+ var compute = new Compute(options);
+
+ assert.deepEqual(compute.authConfig, {
+ credentials: options.credentials,
+ keyFile: options.keyFilename,
+ email: options.email,
+ scopes: ['https://www.googleapis.com/auth/compute']
+ });
+ });
+
+ it('should create a makeAuthorizedRequest method', function(done) {
+ fakeUtil.makeAuthorizedRequestFactory = function(options_) {
+ assert.deepEqual(options_, {
+ credentials: options.credentials,
+ email: options.email,
+ keyFile: options.keyFilename,
+ scopes: ['https://www.googleapis.com/auth/compute']
+ });
+ fakeUtil.makeAuthorizedRequestFactory = util.noop;
+ return done;
+ };
+
+ var compute = new Compute(options);
+ compute.makeAuthorizedRequest_();
+ });
+
+ it('should localize the project id', function() {
+ assert.strictEqual(compute.projectId, PROJECT_ID);
+ });
+ });
+
+ describe('createFirewall', function() {
+ it('should throw if a name is not provided', function() {
+ assert.throws(function() {
+ compute.createFirewall({}, assert.ifError);
+ }, /A firewall name must be provided./);
+ });
+
+ it('should throw if config is not provided', function() {
+ assert.throws(function() {
+ compute.createFirewall('name', assert.ifError);
+ }, /A firewall configuration object must be provided./);
+ });
+
+ describe('config.protocols', function() {
+ it('should format protocols', function(done) {
+ var options = {
+ allowed: {
+ IPProtocol: 'http',
+ ports: [8000]
+ },
+ protocols: {
+ https: [8080, 9000],
+ ssh: 22,
+ ftp: []
+ }
+ };
+
+ compute.makeReq_ = function(method, path, query, body) {
+ assert.deepEqual(body.allowed, [
+ { IPProtocol: 'http', ports: [8000] },
+ { IPProtocol: 'https', ports: [8080, 9000] },
+ { IPProtocol: 'ssh', ports: [22] },
+ { IPProtocol: 'ftp' }
+ ]);
+ assert.strictEqual(body.protocols, undefined);
+ done();
+ };
+
+ compute.createFirewall('name', options, assert.ifError);
+ });
+ });
+
+ describe('config.ranges', function() {
+ it('should format ranges to sourceRanges', function(done) {
+ var options = {
+ ranges: '0.0.0.0/0' // non-array to test that it's arrified.
+ };
+
+ compute.makeReq_ = function(method, path, query, body) {
+ assert.deepEqual(body.sourceRanges, [options.ranges]);
+ assert.strictEqual(body.ranges, undefined);
+ done();
+ };
+
+ compute.createFirewall('name', options, assert.ifError);
+ });
+ });
+
+ describe('config.tags', function() {
+ it('should format tags to sourceTags', function(done) {
+ var options = {
+ tags: 'tag' // non-array to test that it's arrified.
+ };
+
+ compute.makeReq_ = function(method, path, query, body) {
+ assert.deepEqual(body.sourceTags, [options.tags]);
+ assert.strictEqual(body.tags, undefined);
+ done();
+ };
+
+ compute.createFirewall('name', options, assert.ifError);
+ });
+ });
+
+ it('should make the correct API request', function(done) {
+ var name = 'new-firewall-name';
+
+ compute.makeReq_ = function(method, path, query, body) {
+ assert.strictEqual(method, 'POST');
+ assert.strictEqual(path, '/global/firewalls');
+ assert.strictEqual(query, null);
+ assert.deepEqual(body, { name: name });
+ done();
+ };
+
+ compute.createFirewall(name, {}, assert.ifError);
+ });
+
+ describe('error', function() {
+ var error = new Error('Error.');
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ compute.makeReq_ = function(method, path, query, body, callback) {
+ callback(error, apiResponse);
+ };
+ });
+
+ it('should exec the callback with error & API response', function(done) {
+ compute.createFirewall('name', {}, function(err, firewall, op, resp) {
+ assert.strictEqual(err, error);
+ assert.strictEqual(firewall, null);
+ assert.strictEqual(op, null);
+ assert.strictEqual(resp, apiResponse);
+ done();
+ });
+ });
+ });
+
+ describe('success', function() {
+ var apiResponse = {
+ name: 'op-name'
+ };
+
+ beforeEach(function() {
+ compute.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponse);
+ };
+ });
+
+ it('should exec cb with Firewall, Operation & apiResp', function(done) {
+ var name = 'name';
+ var firewall = {};
+ var operation = {};
+
+ compute.firewall = function(name_) {
+ assert.strictEqual(name_, name);
+ return firewall;
+ };
+
+ compute.operation = function(name_) {
+ assert.strictEqual(name_, apiResponse.name);
+ return operation;
+ };
+
+ compute.createFirewall('name', {}, function(err, fw, op, resp) {
+ assert.strictEqual(err, null);
+ assert.strictEqual(fw, firewall);
+ assert.strictEqual(op, operation);
+ assert.strictEqual(op.metadata, apiResponse);
+ assert.strictEqual(resp, apiResponse);
+ done();
+ });
+ });
+ });
+ });
+
+ describe('createNetwork', function() {
+ describe('config.range', function() {
+ it('should set the IPv4Range', function(done) {
+ var options = {
+ range: '10.240.0.0/16'
+ };
+
+ compute.makeReq_ = function(method, path, query, body) {
+ assert.strictEqual(body.IPv4Range, options.range);
+ assert.strictEqual(body.range, undefined);
+ done();
+ };
+
+ compute.createNetwork('name', options, assert.ifError);
+ });
+ });
+
+ describe('config.gateway', function() {
+ it('should set the gatewayIPv4', function(done) {
+ var options = {
+ gateway: '10.1.1.1'
+ };
+
+ compute.makeReq_ = function(method, path, query, body) {
+ assert.strictEqual(body.gatewayIPv4, options.gateway);
+ assert.strictEqual(body.gateway, undefined);
+ done();
+ };
+
+ compute.createNetwork('name', options, assert.ifError);
+ });
+ });
+
+ it('should make the correct API request', function(done) {
+ var name = 'new-network';
+
+ compute.makeReq_ = function(method, path, query, body) {
+ assert.strictEqual(method, 'POST');
+ assert.strictEqual(path, '/global/networks');
+ assert.strictEqual(query, null);
+ assert.deepEqual(body, { name: name });
+ done();
+ };
+
+ compute.createNetwork(name, {}, assert.ifError);
+ });
+
+ describe('error', function() {
+ var error = new Error('Error.');
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ compute.makeReq_ = function(method, path, query, body, callback) {
+ callback(error, apiResponse);
+ };
+ });
+
+ it('should exec the callback with error & API response', function(done) {
+ compute.createNetwork('name', {}, function(err, network, op, resp) {
+ assert.strictEqual(err, error);
+ assert.strictEqual(network, null);
+ assert.strictEqual(op, null);
+ assert.strictEqual(resp, apiResponse);
+ done();
+ });
+ });
+ });
+
+ describe('success', function() {
+ var NAME = 'network-name';
+ var apiResponse = {
+ name: 'op-name'
+ };
+
+ beforeEach(function() {
+ compute.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponse);
+ };
+ });
+
+ it('should exec cb with Network, Operation & apiResp', function(done) {
+ var network = {};
+ var operation = {};
+
+ compute.network = function(name_) {
+ assert.strictEqual(name_, NAME);
+ return network;
+ };
+
+ compute.operation = function(name_) {
+ assert.strictEqual(name_, apiResponse.name);
+ return operation;
+ };
+
+ compute.createNetwork(NAME, {}, function(err, network_, op, resp) {
+ assert.strictEqual(err, null);
+ assert.strictEqual(network_, network);
+ assert.strictEqual(op, operation);
+ assert.strictEqual(op.metadata, apiResponse);
+ assert.strictEqual(resp, apiResponse);
+ done();
+ });
+ });
+ });
+ });
+
+ describe('firewall', function() {
+ var NAME = 'firewall-name';
+
+ it('should return a Firewall object', function() {
+ var firewall = compute.firewall(NAME);
+ assert(firewall instanceof FakeFirewall);
+ assert.strictEqual(firewall.calledWith_[0], compute);
+ assert.strictEqual(firewall.calledWith_[1], NAME);
+ });
+ });
+
+ describe('getAddresses', function() {
+ it('should accept only a callback', function(done) {
+ compute.makeReq_ = function(method, path, query) {
+ assert.deepEqual(query, {});
+ done();
+ };
+
+ compute.getAddresses(assert.ifError);
+ });
+
+ it('should make the correct API request', function(done) {
+ var options = {};
+
+ compute.makeReq_ = function(method, path, query, body) {
+ assert.strictEqual(method, 'GET');
+ assert.strictEqual(path, '/aggregated/addresses');
+ assert.strictEqual(query, options);
+ assert.strictEqual(body, null);
+ done();
+ };
+
+ compute.getAddresses(options, assert.ifError);
+ });
+
+ describe('error', function() {
+ var error = new Error('Error.');
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ compute.makeReq_ = function(method, path, query, body, callback) {
+ callback(error, apiResponse);
+ };
+ });
+
+ it('should execute callback with error & API response', function(done) {
+ compute.getAddresses({}, function(err, addresses, nextQuery, resp) {
+ assert.strictEqual(err, error);
+ assert.strictEqual(addresses, null);
+ assert.strictEqual(nextQuery, null);
+ assert.strictEqual(resp, apiResponse);
+
+ done();
+ });
+ });
+ });
+
+ describe('success', function() {
+ var REGION_NAME = 'region-1';
+ var FULL_REGION_NAME = 'regions/' + REGION_NAME;
+
+ var address = { name: 'address-1' };
+ var apiResponse = {
+ items: {}
+ };
+
+ apiResponse.items[FULL_REGION_NAME] = {
+ addresses: [address]
+ };
+
+ beforeEach(function() {
+ compute.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponse);
+ };
+ });
+
+ it('should create Address objects from the response', function(done) {
+ var region = {};
+
+ compute.region = function(name) {
+ assert.strictEqual(name, REGION_NAME);
+ return region;
+ };
+
+ region.address = function(name) {
+ assert.strictEqual(name, address.name);
+ setImmediate(done);
+ return address;
+ };
+
+ compute.getAddresses({}, assert.ifError);
+ });
+
+ it('should build a nextQuery if necessary', function(done) {
+ var apiResponseWithNextPageToken = extend({}, apiResponse, {
+ nextPageToken: 'next-page-token'
+ });
+
+ var query = { a: 'b', c: 'd' };
+ var originalQuery = extend({}, query);
+
+ compute.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponseWithNextPageToken);
+ };
+
+ compute.getAddresses(query, function(err, addresses, nextQuery) {
+ assert.ifError(err);
+
+ assert.deepEqual(query, originalQuery);
+
+ assert.deepEqual(nextQuery, extend({}, query, {
+ pageToken: apiResponseWithNextPageToken.nextPageToken
+ }));
+
+ done();
+ });
+ });
+ });
+ });
+
+ describe('getDisks', function() {
+ it('should accept only a callback', function(done) {
+ compute.makeReq_ = function(method, path, query) {
+ assert.deepEqual(query, {});
+ done();
+ };
+
+ compute.getDisks(assert.ifError);
+ });
+
+ it('should make the correct API request', function(done) {
+ var options = {};
+
+ compute.makeReq_ = function(method, path, query, body) {
+ assert.strictEqual(method, 'GET');
+ assert.strictEqual(path, '/aggregated/disks');
+ assert.strictEqual(query, options);
+ assert.strictEqual(body, null);
+ done();
+ };
+
+ compute.getDisks(options, assert.ifError);
+ });
+
+ describe('error', function() {
+ var error = new Error('Error.');
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ compute.makeReq_ = function(method, path, query, body, callback) {
+ callback(error, apiResponse);
+ };
+ });
+
+ it('should execute callback with error & API response', function(done) {
+ compute.getDisks({}, function(err, disks, nextQuery, resp) {
+ assert.strictEqual(err, error);
+ assert.strictEqual(disks, null);
+ assert.strictEqual(nextQuery, null);
+ assert.strictEqual(resp, apiResponse);
+
+ done();
+ });
+ });
+ });
+
+ describe('success', function() {
+ var ZONE_NAME = 'zone-1';
+ var FULL_ZONE_NAME = 'zones/' + ZONE_NAME;
+
+ var disk = { name: 'disk-1' };
+ var apiResponse = {
+ items: {}
+ };
+
+ apiResponse.items[FULL_ZONE_NAME] = {
+ disks: [disk]
+ };
+
+ beforeEach(function() {
+ compute.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponse);
+ };
+ });
+
+ it('should create Disk objects from the response', function(done) {
+ var zone = {};
+
+ compute.zone = function(name) {
+ assert.strictEqual(name, ZONE_NAME);
+ return zone;
+ };
+
+ zone.disk = function(name) {
+ assert.strictEqual(name, disk.name);
+ setImmediate(done);
+ return disk;
+ };
+
+ compute.getDisks({}, assert.ifError);
+ });
+
+ it('should build a nextQuery if necessary', function(done) {
+ var apiResponseWithNextPageToken = extend({}, apiResponse, {
+ nextPageToken: 'next-page-token'
+ });
+
+ var query = { a: 'b', c: 'd' };
+ var originalQuery = extend({}, query);
+
+ compute.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponseWithNextPageToken);
+ };
+
+ compute.getDisks(query, function(err, addresses, nextQuery) {
+ assert.ifError(err);
+
+ assert.deepEqual(query, originalQuery);
+
+ assert.deepEqual(nextQuery, extend({}, query, {
+ pageToken: apiResponseWithNextPageToken.nextPageToken
+ }));
+
+ done();
+ });
+ });
+ });
+ });
+
+ describe('getFirewalls', function() {
+ it('should accept only a callback', function(done) {
+ compute.makeReq_ = function(method, path, query) {
+ assert.deepEqual(query, {});
+ done();
+ };
+
+ compute.getFirewalls(assert.ifError);
+ });
+
+ it('should make the correct API request', function(done) {
+ var options = {};
+
+ compute.makeReq_ = function(method, path, query, body) {
+ assert.strictEqual(method, 'GET');
+ assert.strictEqual(path, '/global/firewalls');
+ assert.strictEqual(query, options);
+ assert.strictEqual(body, null);
+ done();
+ };
+
+ compute.getFirewalls(options, assert.ifError);
+ });
+
+ describe('error', function() {
+ var error = new Error('Error.');
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ compute.makeReq_ = function(method, path, query, body, callback) {
+ callback(error, apiResponse);
+ };
+ });
+
+ it('should execute callback with error & API response', function(done) {
+ compute.getFirewalls({}, function(err, firewalls, nextQuery, resp) {
+ assert.strictEqual(err, error);
+ assert.strictEqual(firewalls, null);
+ assert.strictEqual(nextQuery, null);
+ assert.strictEqual(resp, apiResponse);
+
+ done();
+ });
+ });
+ });
+
+ describe('success', function() {
+ var firewall = { name: 'firewall-1' };
+ var apiResponse = {
+ items: [firewall]
+ };
+
+ beforeEach(function() {
+ compute.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponse);
+ };
+ });
+
+ it('should create Firewall objects from the response', function(done) {
+ compute.firewall = function(name) {
+ assert.strictEqual(name, firewall.name);
+ setImmediate(done);
+ return firewall;
+ };
+
+ compute.getFirewalls({}, assert.ifError);
+ });
+
+ it('should build a nextQuery if necessary', function(done) {
+ var apiResponseWithNextPageToken = extend({}, apiResponse, {
+ nextPageToken: 'next-page-token'
+ });
+
+ var query = { a: 'b', c: 'd' };
+ var originalQuery = extend({}, query);
+
+ compute.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponseWithNextPageToken);
+ };
+
+ compute.getFirewalls(query, function(err, addresses, nextQuery) {
+ assert.ifError(err);
+
+ assert.deepEqual(query, originalQuery);
+
+ assert.deepEqual(nextQuery, extend({}, query, {
+ pageToken: apiResponseWithNextPageToken.nextPageToken
+ }));
+
+ done();
+ });
+ });
+ });
+ });
+
+ describe('getNetworks', function() {
+ it('should work with only a callback', function(done) {
+ compute.makeReq_ = function(method, path, query) {
+ assert.deepEqual(query, {});
+ done();
+ };
+
+ compute.getNetworks(assert.ifError);
+ });
+
+ it('should make the correct API request', function(done) {
+ var options = {};
+
+ compute.makeReq_ = function(method, path, query, body) {
+ assert.strictEqual(method, 'GET');
+ assert.strictEqual(path, '/global/networks');
+ assert.strictEqual(query, options);
+ assert.strictEqual(body, null);
+ done();
+ };
+
+ compute.getNetworks(options, assert.ifError);
+ });
+
+ describe('error', function() {
+ var error = new Error('Error.');
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ compute.makeReq_ = function(method, path, query, body, callback) {
+ callback(error, apiResponse);
+ };
+ });
+
+ it('should execute callback with error & API response', function(done) {
+ compute.getNetworks({}, function(err, networks, nextQuery, resp) {
+ assert.strictEqual(err, error);
+ assert.strictEqual(networks, null);
+ assert.strictEqual(nextQuery, null);
+ assert.strictEqual(resp, apiResponse);
+
+ done();
+ });
+ });
+ });
+
+ describe('success', function() {
+ var network = { name: 'network-1' };
+ var apiResponse = {
+ items: [network]
+ };
+
+ beforeEach(function() {
+ compute.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponse);
+ };
+ });
+
+ it('should create Network objects from the response', function(done) {
+ compute.network = function(name) {
+ assert.strictEqual(name, network.name);
+ setImmediate(done);
+ return network;
+ };
+
+ compute.getNetworks({}, assert.ifError);
+ });
+
+ it('should build a nextQuery if necessary', function(done) {
+ var apiResponseWithNextPageToken = extend({}, apiResponse, {
+ nextPageToken: 'next-page-token'
+ });
+
+ var query = { a: 'b', c: 'd' };
+ var originalQuery = extend({}, query);
+
+ compute.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponseWithNextPageToken);
+ };
+
+ compute.getNetworks(query, function(err, addresses, nextQuery) {
+ assert.ifError(err);
+
+ assert.deepEqual(query, originalQuery);
+
+ assert.deepEqual(nextQuery, extend({}, query, {
+ pageToken: apiResponseWithNextPageToken.nextPageToken
+ }));
+
+ done();
+ });
+ });
+ });
+ });
+
+ describe('getOperations', function() {
+ it('should work with only a callback', function(done) {
+ compute.makeReq_ = function(method, path, query) {
+ assert.deepEqual(query, {});
+ done();
+ };
+
+ compute.getOperations(assert.ifError);
+ });
+
+ it('should make the correct API request', function(done) {
+ var options = {};
+
+ compute.makeReq_ = function(method, path, query, body) {
+ assert.strictEqual(method, 'GET');
+ assert.strictEqual(path, '/global/operations');
+ assert.strictEqual(query, options);
+ assert.strictEqual(body, null);
+ done();
+ };
+
+ compute.getOperations(options, assert.ifError);
+ });
+
+ describe('error', function() {
+ var error = new Error('Error.');
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ compute.makeReq_ = function(method, path, query, body, callback) {
+ callback(error, apiResponse);
+ };
+ });
+
+ it('should execute callback with error & API response', function(done) {
+ compute.getOperations({}, function(err, ops, nextQuery, resp) {
+ assert.strictEqual(err, error);
+ assert.strictEqual(ops, null);
+ assert.strictEqual(nextQuery, null);
+ assert.strictEqual(resp, apiResponse);
+
+ done();
+ });
+ });
+ });
+
+ describe('success', function() {
+ var operation = { name: 'op-1' };
+ var apiResponse = {
+ items: [operation]
+ };
+
+ beforeEach(function() {
+ compute.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponse);
+ };
+ });
+
+ it('should create Operation objects from the response', function(done) {
+ compute.operation = function(name) {
+ assert.strictEqual(name, operation.name);
+ setImmediate(done);
+ return operation;
+ };
+
+ compute.getOperations({}, assert.ifError);
+ });
+
+ it('should build a nextQuery if necessary', function(done) {
+ var apiResponseWithNextPageToken = extend({}, apiResponse, {
+ nextPageToken: 'next-page-token'
+ });
+
+ var query = { a: 'b', c: 'd' };
+ var originalQuery = extend({}, query);
+
+ compute.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponseWithNextPageToken);
+ };
+
+ compute.getOperations(query, function(err, addresses, nextQuery) {
+ assert.ifError(err);
+
+ assert.deepEqual(query, originalQuery);
+
+ assert.deepEqual(nextQuery, extend({}, query, {
+ pageToken: apiResponseWithNextPageToken.nextPageToken
+ }));
+
+ done();
+ });
+ });
+ });
+ });
+
+ describe('getRegions', function() {
+ it('should work with only a callback', function(done) {
+ compute.makeReq_ = function(method, path, query) {
+ assert.deepEqual(query, {});
+ done();
+ };
+
+ compute.getRegions(assert.ifError);
+ });
+
+ it('should make the correct API request', function(done) {
+ var options = {};
+
+ compute.makeReq_ = function(method, path, query, body) {
+ assert.strictEqual(method, 'GET');
+ assert.strictEqual(path, '/regions');
+ assert.strictEqual(query, options);
+ assert.strictEqual(body, null);
+ done();
+ };
+
+ compute.getRegions(options, assert.ifError);
+ });
+
+ describe('error', function() {
+ var error = new Error('Error.');
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ compute.makeReq_ = function(method, path, query, body, callback) {
+ callback(error, apiResponse);
+ };
+ });
+
+ it('should execute callback with error & API response', function(done) {
+ compute.getRegions({}, function(err, regions, nextQuery, resp) {
+ assert.strictEqual(err, error);
+ assert.strictEqual(regions, null);
+ assert.strictEqual(nextQuery, null);
+ assert.strictEqual(resp, apiResponse);
+
+ done();
+ });
+ });
+ });
+
+ describe('success', function() {
+ var region = { name: 'region-1' };
+ var apiResponse = {
+ items: [region]
+ };
+
+ beforeEach(function() {
+ compute.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponse);
+ };
+ });
+
+ it('should create Region objects from the response', function(done) {
+ compute.region = function(name) {
+ assert.strictEqual(name, region.name);
+ setImmediate(done);
+ return region;
+ };
+
+ compute.getRegions({}, assert.ifError);
+ });
+
+ it('should build a nextQuery if necessary', function(done) {
+ var apiResponseWithNextPageToken = extend({}, apiResponse, {
+ nextPageToken: 'next-page-token'
+ });
+
+ var query = { a: 'b', c: 'd' };
+ var originalQuery = extend({}, query);
+
+ compute.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponseWithNextPageToken);
+ };
+
+ compute.getRegions(query, function(err, addresses, nextQuery) {
+ assert.ifError(err);
+
+ assert.deepEqual(query, originalQuery);
+
+ assert.deepEqual(nextQuery, extend({}, query, {
+ pageToken: apiResponseWithNextPageToken.nextPageToken
+ }));
+
+ done();
+ });
+ });
+ });
+ });
+
+ describe('getSnapshots', function() {
+ it('should work with only a callback', function(done) {
+ compute.makeReq_ = function(method, path, query) {
+ assert.deepEqual(query, {});
+ done();
+ };
+
+ compute.getSnapshots(assert.ifError);
+ });
+
+ it('should make the correct API request', function(done) {
+ var options = {};
+
+ compute.makeReq_ = function(method, path, query, body) {
+ assert.strictEqual(method, 'GET');
+ assert.strictEqual(path, '/global/snapshots');
+ assert.strictEqual(query, options);
+ assert.strictEqual(body, null);
+ done();
+ };
+
+ compute.getSnapshots(options, assert.ifError);
+ });
+
+ describe('error', function() {
+ var error = new Error('Error.');
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ compute.makeReq_ = function(method, path, query, body, callback) {
+ callback(error, apiResponse);
+ };
+ });
+
+ it('should execute callback with error & API response', function(done) {
+ compute.getSnapshots({}, function(err, snapshots, nextQuery, resp) {
+ assert.strictEqual(err, error);
+ assert.strictEqual(snapshots, null);
+ assert.strictEqual(nextQuery, null);
+ assert.strictEqual(resp, apiResponse);
+
+ done();
+ });
+ });
+ });
+
+ describe('success', function() {
+ var snapshot = { name: 'snapshot-1' };
+ var apiResponse = {
+ items: [snapshot]
+ };
+
+ beforeEach(function() {
+ compute.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponse);
+ };
+ });
+
+ it('should create Snapshot objects from the response', function(done) {
+ compute.snapshot = function(name) {
+ assert.strictEqual(name, snapshot.name);
+ setImmediate(done);
+ return snapshot;
+ };
+
+ compute.getSnapshots({}, assert.ifError);
+ });
+
+ it('should build a nextQuery if necessary', function(done) {
+ var apiResponseWithNextPageToken = extend({}, apiResponse, {
+ nextPageToken: 'next-page-token'
+ });
+
+ var query = { a: 'b', c: 'd' };
+ var originalQuery = extend({}, query);
+
+ compute.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponseWithNextPageToken);
+ };
+
+ compute.getSnapshots(query, function(err, snapshots, nextQuery) {
+ assert.ifError(err);
+
+ assert.deepEqual(query, originalQuery);
+
+ assert.deepEqual(nextQuery, extend({}, query, {
+ pageToken: apiResponseWithNextPageToken.nextPageToken
+ }));
+
+ done();
+ });
+ });
+ });
+ });
+
+ describe('getVMs', function() {
+ it('should work with only a callback', function(done) {
+ compute.makeReq_ = function(method, path, query) {
+ assert.deepEqual(query, {});
+ done();
+ };
+
+ compute.getVMs(assert.ifError);
+ });
+
+ it('should make the correct API request', function(done) {
+ var options = {};
+
+ compute.makeReq_ = function(method, path, query, body) {
+ assert.strictEqual(method, 'GET');
+ assert.strictEqual(path, '/aggregated/instances');
+ assert.strictEqual(query, options);
+ assert.strictEqual(body, null);
+ done();
+ };
+
+ compute.getVMs(options, assert.ifError);
+ });
+
+ describe('error', function() {
+ var error = new Error('Error.');
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ compute.makeReq_ = function(method, path, query, body, callback) {
+ callback(error, apiResponse);
+ };
+ });
+
+ it('should execute callback with error & API response', function(done) {
+ compute.getVMs({}, function(err, vms, nextQuery, resp) {
+ assert.strictEqual(err, error);
+ assert.strictEqual(vms, null);
+ assert.strictEqual(nextQuery, null);
+ assert.strictEqual(resp, apiResponse);
+
+ done();
+ });
+ });
+ });
+
+ describe('success', function() {
+ var ZONE_NAME = 'zone-1';
+ var FULL_ZONE_NAME = 'zones/' + ZONE_NAME;
+
+ var vm = { name: 'vm-1' };
+ var apiResponse = {
+ items: {}
+ };
+
+ apiResponse.items[FULL_ZONE_NAME] = {
+ instances: [vm]
+ };
+
+ beforeEach(function() {
+ compute.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponse);
+ };
+ });
+
+ it('should create VM objects from the response', function(done) {
+ var zone = {};
+
+ compute.zone = function(name) {
+ assert.strictEqual(name, ZONE_NAME);
+ return zone;
+ };
+
+ zone.vm = function(name) {
+ assert.strictEqual(name, vm.name);
+ setImmediate(done);
+ return vm;
+ };
+
+ compute.getVMs({}, assert.ifError);
+ });
+
+ it('should build a nextQuery if necessary', function(done) {
+ var apiResponseWithNextPageToken = extend({}, apiResponse, {
+ nextPageToken: 'next-page-token'
+ });
+
+ var query = { a: 'b', c: 'd' };
+ var originalQuery = extend({}, query);
+
+ compute.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponseWithNextPageToken);
+ };
+
+ compute.getVMs(query, function(err, addresses, nextQuery) {
+ assert.ifError(err);
+
+ assert.deepEqual(query, originalQuery);
+
+ assert.deepEqual(nextQuery, extend({}, query, {
+ pageToken: apiResponseWithNextPageToken.nextPageToken
+ }));
+
+ done();
+ });
+ });
+ });
+ });
+
+ describe('getZones', function() {
+ it('should work with only a callback', function(done) {
+ compute.makeReq_ = function(method, path, query) {
+ assert.deepEqual(query, {});
+ done();
+ };
+
+ compute.getZones(assert.ifError);
+ });
+
+ it('should make the correct API request', function(done) {
+ var options = {};
+
+ compute.makeReq_ = function(method, path, query, body) {
+ assert.strictEqual(method, 'GET');
+ assert.strictEqual(path, '/zones');
+ assert.strictEqual(query, options);
+ assert.strictEqual(body, null);
+ done();
+ };
+
+ compute.getZones(options, assert.ifError);
+ });
+
+ describe('error', function() {
+ var error = new Error('Error.');
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ compute.makeReq_ = function(method, path, query, body, callback) {
+ callback(error, apiResponse);
+ };
+ });
+
+ it('should execute callback with error & API response', function(done) {
+ compute.getZones({}, function(err, zones, nextQuery, resp) {
+ assert.strictEqual(err, error);
+ assert.strictEqual(zones, null);
+ assert.strictEqual(nextQuery, null);
+ assert.strictEqual(resp, apiResponse);
+
+ done();
+ });
+ });
+ });
+
+ describe('success', function() {
+ var zone = { name: 'zone-1' };
+ var apiResponse = {
+ items: [zone]
+ };
+
+ beforeEach(function() {
+ compute.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponse);
+ };
+ });
+
+ it('should create Zone objects from the response', function(done) {
+ compute.zone = function(name) {
+ assert.strictEqual(name, zone.name);
+ setImmediate(done);
+ return zone;
+ };
+
+ compute.getZones({}, assert.ifError);
+ });
+
+ it('should build a nextQuery if necessary', function(done) {
+ var apiResponseWithNextPageToken = extend({}, apiResponse, {
+ nextPageToken: 'next-page-token'
+ });
+
+ var query = { a: 'b', c: 'd' };
+ var originalQuery = extend({}, query);
+
+ compute.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponseWithNextPageToken);
+ };
+
+ compute.getZones(query, function(err, snapshots, nextQuery) {
+ assert.ifError(err);
+
+ assert.deepEqual(query, originalQuery);
+
+ assert.deepEqual(nextQuery, extend({}, query, {
+ pageToken: apiResponseWithNextPageToken.nextPageToken
+ }));
+
+ done();
+ });
+ });
+ });
+ });
+
+ describe('network', function() {
+ var NAME = 'network-name';
+
+ it('should return a Network object', function() {
+ var network = compute.network(NAME);
+ assert(network instanceof FakeNetwork);
+ assert.strictEqual(network.calledWith_[0], compute);
+ assert.strictEqual(network.calledWith_[1], NAME);
+ });
+ });
+
+ describe('operation', function() {
+ var NAME = 'op-name';
+
+ it('should return an Operation object', function() {
+ var op = compute.operation(NAME);
+ assert(op instanceof FakeOperation);
+ assert.strictEqual(op.calledWith_[0], compute);
+ assert.strictEqual(op.calledWith_[1], NAME);
+ });
+ });
+
+ describe('region', function() {
+ var NAME = 'region-name';
+
+ it('should return a Region object', function() {
+ var region = compute.region(NAME);
+ assert(region instanceof FakeRegion);
+ assert.strictEqual(region.calledWith_[0], compute);
+ assert.strictEqual(region.calledWith_[1], NAME);
+ });
+ });
+
+ describe('snapshot', function() {
+ var NAME = 'snapshot-name';
+
+ it('should return a Snapshot object', function() {
+ var snapshot = compute.snapshot(NAME);
+ assert(snapshot instanceof FakeSnapshot);
+ assert.strictEqual(snapshot.calledWith_[0], compute);
+ assert.strictEqual(snapshot.calledWith_[1], NAME);
+ });
+ });
+
+ describe('zone', function() {
+ var NAME = 'zone-name';
+
+ it('should return a Zone object', function() {
+ var zone = compute.zone(NAME);
+ assert(zone instanceof FakeZone);
+ assert.strictEqual(zone.calledWith_[0], compute);
+ assert.strictEqual(zone.calledWith_[1], NAME);
+ });
+ });
+
+ describe('makeReq_', function() {
+ it('should make the correct request to Compute', function(done) {
+ var method = 'POST';
+ var path = '/';
+ var query = 'query';
+ var body = 'body';
+
+ compute.makeAuthorizedRequest_ = function(reqOpts, callback) {
+ assert.equal(reqOpts.method, method);
+ assert.equal(reqOpts.qs, query);
+
+ var baseUri = 'https://www.googleapis.com/compute/v1/';
+ assert.equal(reqOpts.uri, baseUri + 'projects/' + PROJECT_ID + path);
+
+ assert.equal(reqOpts.json, body);
+
+ callback();
+ };
+
+ compute.makeReq_(method, path, query, body, done);
+ });
+ });
+});
diff --git a/test/compute/network.js b/test/compute/network.js
new file mode 100644
index 00000000000..893f46fb344
--- /dev/null
+++ b/test/compute/network.js
@@ -0,0 +1,334 @@
+/**
+ * Copyright 2015 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.
+ */
+
+'use strict';
+
+var assert = require('assert');
+var extend = require('extend');
+var format = require('string-format-obj');
+
+var Network = require('../../lib/compute/network.js');
+
+describe('Network', function() {
+ var network;
+
+ var COMPUTE = { projectId: 'project-id' };
+ var NETWORK_NAME = 'network-name';
+ var NETWORK_FULL_NAME = format('projects/{pId}/global/networks/{name}', {
+ pId: COMPUTE.projectId,
+ name: NETWORK_NAME
+ });
+
+ beforeEach(function() {
+ network = new Network(COMPUTE, NETWORK_NAME);
+ });
+
+ describe('instantiation', function() {
+ it('should localize the compute instance', function() {
+ assert.strictEqual(network.compute, COMPUTE);
+ });
+
+ it('should localize the name', function() {
+ assert.strictEqual(network.name, NETWORK_NAME);
+ });
+
+ it('should default metadata to an empty object', function() {
+ assert.strictEqual(typeof network.metadata, 'object');
+ assert.strictEqual(Object.keys(network.metadata).length, 0);
+ });
+
+ it('should format the network name', function() {
+ var formatName_ = Network.formatName_;
+ var formattedName = 'projects/a/global/networks/b';
+
+ Network.formatName_ = function(compute, name) {
+ Network.formatName_ = formatName_;
+
+ assert.strictEqual(compute, COMPUTE);
+ assert.strictEqual(name, NETWORK_NAME);
+
+ return formattedName;
+ };
+
+ var network = new Network(COMPUTE, NETWORK_NAME);
+ assert(network.formattedName, formattedName);
+ });
+ });
+
+ describe('formatName_', function() {
+ it('should format the name', function() {
+ var formattedName_ = Network.formatName_(COMPUTE, NETWORK_NAME);
+ assert.strictEqual(formattedName_, NETWORK_FULL_NAME);
+ });
+ });
+
+ describe('createFirewall', function() {
+ it('should make the correct call to Compute', function(done) {
+ var name = 'firewall-name';
+ var config = { a: 'b', c: 'd' };
+ var expectedConfig = extend({}, config, {
+ network: network.formattedName
+ });
+
+ network.compute.createFirewall = function(name_, config_, callback) {
+ assert.strictEqual(name_, name);
+ assert.deepEqual(config_, expectedConfig);
+ callback();
+ };
+
+ network.createFirewall(name, config, done);
+ });
+ });
+
+ describe('delete', function() {
+ it('should make the correct API request', function(done) {
+ network.makeReq_ = function(method, path, query, body) {
+ assert.strictEqual(method, 'DELETE');
+ assert.strictEqual(path, '');
+ assert.strictEqual(query, null);
+ assert.strictEqual(body, null);
+ done();
+ };
+
+ network.delete(assert.ifError);
+ });
+
+ describe('error', function() {
+ var error = new Error('Error.');
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ network.makeReq_ = function(method, path, query, body, callback) {
+ callback(error, apiResponse);
+ };
+ });
+
+ it('should return an error if the request fails', function(done) {
+ network.delete(function(err, operation, apiResponse_) {
+ assert.strictEqual(err, error);
+ assert.strictEqual(operation, null);
+ assert.strictEqual(apiResponse_, apiResponse);
+ done();
+ });
+ });
+
+ it('should not require a callback', function() {
+ assert.doesNotThrow(function() {
+ network.delete();
+ });
+ });
+ });
+
+ describe('success', function() {
+ var apiResponse = {
+ name: 'op-name'
+ };
+
+ beforeEach(function() {
+ network.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponse);
+ };
+ });
+
+ it('should execute callback with Operation & Response', function(done) {
+ var operation = {};
+
+ network.compute.operation = function(name) {
+ assert.strictEqual(name, apiResponse.name);
+ return operation;
+ };
+
+ network.delete(function(err, operation_, apiResponse_) {
+ assert.ifError(err);
+ assert.strictEqual(operation_, operation);
+ assert.strictEqual(operation_.metadata, apiResponse);
+ assert.strictEqual(apiResponse_, apiResponse);
+ done();
+ });
+ });
+
+ it('should not require a callback', function() {
+ assert.doesNotThrow(function() {
+ network.delete();
+ });
+ });
+ });
+ });
+
+ describe('firewall', function() {
+ it('should return a Firewall with the correct metadata', function() {
+ var name = 'firewall-name';
+ var firewall = {};
+
+ network.compute.firewall = function(name_) {
+ assert.strictEqual(name_, name);
+ return firewall;
+ };
+
+ var firewallInstance = network.firewall(name);
+ assert.deepEqual(firewallInstance.metadata, {
+ network: network.formattedName
+ });
+ });
+ });
+
+ describe('getFirewalls', function() {
+ it('should make the correct call to Compute', function(done) {
+ var options = { a: 'b', c: 'd' };
+ var expectedOptions = extend({}, options, {
+ filter: 'network eq .*' + network.formattedName
+ });
+
+ network.compute.getFirewalls = function(options, callback) {
+ assert.deepEqual(options, expectedOptions);
+ callback();
+ };
+
+ network.getFirewalls(options, done);
+ });
+
+ it('should not require options', function(done) {
+ network.compute.getFirewalls = function(options, callback) {
+ callback();
+ };
+
+ network.getFirewalls(done);
+ });
+
+ it('should not require any arguments', function(done) {
+ network.compute.getFirewalls = function(options, callback) {
+ assert.deepEqual(options, {
+ filter: 'network eq .*' + network.formattedName
+ });
+ assert.strictEqual(typeof callback, 'undefined');
+ done();
+ };
+
+ network.getFirewalls();
+ });
+
+ it('should return the result of calling Compute', function() {
+ var resultOfGetFirewalls = {};
+
+ network.compute.getFirewalls = function() {
+ return resultOfGetFirewalls;
+ };
+
+ assert.strictEqual(network.getFirewalls(), resultOfGetFirewalls);
+ });
+ });
+
+ describe('getMetadata', function() {
+ it('should make the correct API request', function(done) {
+ network.makeReq_ = function(method, path, query, body) {
+ assert.strictEqual(method, 'GET');
+ assert.strictEqual(path, '');
+ assert.strictEqual(query, null);
+ assert.strictEqual(body, null);
+
+ done();
+ };
+
+ network.getMetadata(assert.ifError);
+ });
+
+ describe('error', function() {
+ var error = new Error('Error.');
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ network.makeReq_ = function(method, path, query, body, callback) {
+ callback(error, apiResponse);
+ };
+ });
+
+ it('should execute callback with error and API response', function(done) {
+ network.getMetadata(function(err, metadata, apiResponse_) {
+ assert.strictEqual(err, error);
+ assert.strictEqual(metadata, null);
+ assert.strictEqual(apiResponse_, apiResponse);
+ done();
+ });
+ });
+
+ it('should not require a callback', function() {
+ assert.doesNotThrow(function() {
+ network.getMetadata();
+ });
+ });
+ });
+
+ describe('success', function() {
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ network.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponse);
+ };
+ });
+
+ it('should update the metadata to the API response', function(done) {
+ network.getMetadata(function(err) {
+ assert.ifError(err);
+ assert.strictEqual(network.metadata, apiResponse);
+ done();
+ });
+ });
+
+ it('should exec callback with metadata and API response', function(done) {
+ network.getMetadata(function(err, metadata, apiResponse_) {
+ assert.ifError(err);
+ assert.strictEqual(metadata, apiResponse);
+ assert.strictEqual(apiResponse_, apiResponse);
+ done();
+ });
+ });
+
+ it('should not require a callback', function() {
+ assert.doesNotThrow(function() {
+ network.getMetadata();
+ });
+ });
+ });
+ });
+
+ describe('makeReq_', function() {
+ it('should make the correct request to Compute', function(done) {
+ var expectedPathPrefix = '/global/networks/' + network.name;
+
+ var method = 'POST';
+ var path = '/test';
+ var query = {
+ a: 'b',
+ c: 'd'
+ };
+ var body = {
+ a: 'b',
+ c: 'd'
+ };
+
+ network.compute.makeReq_ = function(method_, path_, query_, body_, cb) {
+ assert.strictEqual(method_, method);
+ assert.strictEqual(path_, expectedPathPrefix + path);
+ assert.strictEqual(query_, query);
+ assert.strictEqual(body_, body);
+ cb();
+ };
+
+ network.makeReq_(method, path, query, body, done);
+ });
+ });
+});
diff --git a/test/compute/operation.js b/test/compute/operation.js
new file mode 100644
index 00000000000..54ad6935eb2
--- /dev/null
+++ b/test/compute/operation.js
@@ -0,0 +1,381 @@
+/**
+ * Copyright 2015 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.
+ */
+
+'use strict';
+
+var assert = require('assert');
+
+var Operation = require('../../lib/compute/operation.js');
+var util = require('../../lib/common/util.js');
+
+describe('Operation', function() {
+ var SCOPE = {};
+ var OPERATION_NAME = 'operation-name';
+
+ var operation;
+
+ beforeEach(function() {
+ operation = new Operation(SCOPE, OPERATION_NAME);
+ });
+
+ describe('instantiation', function() {
+ it('should localize the scope', function() {
+ assert.strictEqual(operation.scope, SCOPE);
+ });
+
+ it('should localize the name', function() {
+ assert.strictEqual(operation.name, OPERATION_NAME);
+ });
+
+ it('should default metadata to an empty object', function() {
+ assert.strictEqual(typeof operation.metadata, 'object');
+ assert.strictEqual(Object.keys(operation.metadata).length, 0);
+ });
+ });
+
+ describe('delete', function() {
+ it('should make the correct API request', function(done) {
+ operation.makeReq_ = function(method, path, query, body) {
+ assert.strictEqual(method, 'DELETE');
+ assert.strictEqual(path, '');
+ assert.strictEqual(query, null);
+ assert.strictEqual(body, null);
+
+ done();
+ };
+
+ operation.delete(assert.ifError);
+ });
+
+ describe('error', function() {
+ var error = new Error('Error.');
+ var apiResponse = {};
+
+ beforeEach(function() {
+ operation.makeReq_ = function(method, path, query, body, callback) {
+ callback(error, apiResponse);
+ };
+ });
+
+ it('should execute callback with error & API resp', function(done) {
+ operation.delete(function(err, apiResponse_) {
+ assert.strictEqual(err, error);
+ assert.strictEqual(apiResponse_, apiResponse);
+ done();
+ });
+ });
+
+ it('should not require a callback', function() {
+ assert.doesNotThrow(function() {
+ operation.delete();
+ });
+ });
+ });
+
+ describe('success', function() {
+ var apiResponse = {};
+
+ beforeEach(function() {
+ operation.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponse);
+ };
+ });
+
+ it('should execute callback with error & API resp', function(done) {
+ operation.delete(function(err, apiResponse_) {
+ assert.ifError(err);
+ assert.strictEqual(apiResponse_, apiResponse);
+ done();
+ });
+ });
+
+ it('should not require a callback', function() {
+ assert.doesNotThrow(function() {
+ operation.delete();
+ });
+ });
+ });
+ });
+
+ describe('getMetadata', function() {
+ it('should make the correct API request', function(done) {
+ operation.makeReq_ = function(method, path, query, body) {
+ assert.strictEqual(method, 'GET');
+ assert.strictEqual(path, '');
+ assert.strictEqual(query, null);
+ assert.strictEqual(body, null);
+
+ done();
+ };
+
+ operation.getMetadata(assert.ifError);
+ });
+
+ describe('error', function() {
+ var error = new Error('Error.');
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ operation.makeReq_ = function(method, path, query, body, callback) {
+ callback(error, apiResponse);
+ };
+ });
+
+ it('should ignore false errors', function(done) {
+ var apiResponse = {
+ name: operation.name,
+ error: {
+ errors: []
+ }
+ };
+
+ operation.makeReq_ = function(method, path, query, body, callback) {
+ callback(apiResponse.error, apiResponse);
+ };
+
+ operation.getMetadata(function(err, metadata, apiResponse_) {
+ assert.ifError(err);
+ assert.strictEqual(metadata, apiResponse);
+ assert.strictEqual(apiResponse_, apiResponse);
+ done();
+ });
+ });
+
+ it('should execute callback with error and API response', function(done) {
+ operation.getMetadata(function(err, metadata, apiResponse_) {
+ assert.strictEqual(err, error);
+ assert.strictEqual(metadata, null);
+ assert.strictEqual(apiResponse_, apiResponse);
+ done();
+ });
+ });
+
+ it('should not require a callback', function() {
+ assert.doesNotThrow(function() {
+ operation.getMetadata();
+ });
+ });
+ });
+
+ describe('success', function() {
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ operation.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponse);
+ };
+ });
+
+ it('should update the metadata to the API response', function(done) {
+ operation.getMetadata(function(err) {
+ assert.ifError(err);
+ assert.strictEqual(operation.metadata, apiResponse);
+ done();
+ });
+ });
+
+ it('should exec callback with metadata and API response', function(done) {
+ operation.getMetadata(function(err, metadata, apiResponse_) {
+ assert.ifError(err);
+ assert.strictEqual(metadata, apiResponse);
+ assert.strictEqual(apiResponse_, apiResponse);
+ done();
+ });
+ });
+
+ it('should not require a callback', function() {
+ assert.doesNotThrow(function() {
+ operation.getMetadata();
+ });
+ });
+ });
+ });
+
+ describe('onComplete', function() {
+ // Set interval to 0 so our tests don't waste time.
+ var OPTIONS = { interval: 0 };
+
+ var error = new Error('Error.');
+ var apiResponse = { a: 'b', c: 'd' };
+ var apiResponseWithIncompleteStatus = { status: 'INCOMPLETE' };
+ var apiResponseWithCompleteStatus = { status: 'DONE' };
+
+ function getMetadataRespondsWithError(callback) {
+ callback(error, apiResponse);
+ }
+
+ function getMetadataRespondsWithIncompleteStatus(callback) {
+ callback(null, apiResponseWithIncompleteStatus);
+ }
+
+ function getMetadataRespondsWithCompleteStatus(callback) {
+ callback(null, apiResponseWithCompleteStatus);
+ }
+
+ describe('options.maxAttempts', function() {
+ it('should default to 10', function(done) {
+ var numAttemptsMade = 0;
+
+ operation.getMetadata = function(callback) {
+ numAttemptsMade++;
+ getMetadataRespondsWithIncompleteStatus(callback);
+ };
+
+ operation.onComplete(OPTIONS, function() {
+ assert.strictEqual(numAttemptsMade, 10);
+ done();
+ });
+ });
+
+ it('should allow overriding', function(done) {
+ var options = { maxAttempts: 3, interval: 0 };
+ var numAttemptsMade = 0;
+
+ operation.getMetadata = function(callback) {
+ numAttemptsMade++;
+ getMetadataRespondsWithIncompleteStatus(callback);
+ };
+
+ operation.onComplete(options, function() {
+ assert.strictEqual(numAttemptsMade, options.maxAttempts);
+ done();
+ });
+ });
+ });
+
+ describe('options.interval', function() {
+ it('should default to 3000ms', function(done) {
+ this.timeout(3100);
+
+ operation.getMetadata = getMetadataRespondsWithIncompleteStatus;
+
+ var started = Date.now();
+ operation.onComplete({ maxAttempts: 1 }, function() {
+ var ended = Date.now();
+
+ assert(ended - started > 2900 && ended - started < 3100);
+ done();
+ });
+ });
+
+ it('should allow overriding', function(done) {
+ operation.getMetadata = getMetadataRespondsWithIncompleteStatus;
+
+ var started = Date.now();
+ operation.onComplete({ maxAttempts: 1, interval: 1000 }, function() {
+ var ended = Date.now();
+
+ assert(ended - started > 900 && ended - started < 1100);
+ done();
+ });
+ });
+ });
+
+ it('should put the interval on the leading side', function(done) {
+ // (It should wait interval before making first request)
+ var started = Date.now();
+ operation.getMetadata = function() {
+ var ended = Date.now();
+
+ assert(ended - started > 900 && ended - started < 1100);
+ done();
+ };
+
+ operation.onComplete({ maxAttempts: 1, interval: 1000 }, util.noop);
+ });
+
+ it('should return an error if maxAttempts is exceeded', function(done) {
+ var options = { maxAttempts: 1, interval: 0 };
+
+ operation.getMetadata = getMetadataRespondsWithIncompleteStatus;
+
+ operation.onComplete(options, function(err, metadata) {
+ assert.strictEqual(err.code, 'OPERATION_INCOMPLETE');
+ assert.strictEqual(err.message, 'Operation did not complete.');
+
+ assert.strictEqual(metadata, operation.metadata);
+ done();
+ });
+ });
+
+ describe('getMetadata', function() {
+ describe('error', function() {
+ it('should execute callback with error & API response', function(done) {
+ operation.getMetadata = getMetadataRespondsWithError;
+
+ operation.onComplete(OPTIONS, function(err, metadata) {
+ assert.strictEqual(err, error);
+ assert.strictEqual(metadata, apiResponse);
+ done();
+ });
+ });
+ });
+
+ describe('success', function() {
+ it('should exec callback with metadata when done', function(done) {
+ operation.getMetadata = getMetadataRespondsWithCompleteStatus;
+
+ operation.onComplete(OPTIONS, function(err, metadata) {
+ assert.ifError(err);
+ assert.strictEqual(metadata, apiResponseWithCompleteStatus);
+ done();
+ });
+ });
+ });
+ });
+ });
+
+ describe('makeReq_', function() {
+ it('should make the correct request to Scope', function(done) {
+ var expectedPathPrefix = '/operations/' + operation.name;
+
+ var method = 'POST';
+ var path = '/test';
+ var query = {
+ a: 'b',
+ c: 'd'
+ };
+ var body = {
+ a: 'b',
+ c: 'd'
+ };
+
+ operation.scope.makeReq_ = function(method_, path_, query_, body_, cb) {
+ assert.strictEqual(method_, method);
+ assert.strictEqual(path_, expectedPathPrefix + path);
+ assert.strictEqual(query_, query);
+ assert.strictEqual(body_, body);
+ cb();
+ };
+
+ operation.makeReq_(method, path, query, body, done);
+ });
+
+ it('should prefix the path with /global if Compute', function(done) {
+ var expectedPathPrefix = '/global/operations/' + operation.name;
+
+ function Compute() {}
+ operation.scope = new Compute();
+
+ operation.scope.makeReq_ = function(method, path) {
+ assert.strictEqual(path, expectedPathPrefix + '/test');
+ done();
+ };
+
+ operation.makeReq_(null, '/test');
+ });
+ });
+});
diff --git a/test/compute/region.js b/test/compute/region.js
new file mode 100644
index 00000000000..f52373d5bb7
--- /dev/null
+++ b/test/compute/region.js
@@ -0,0 +1,512 @@
+/**
+ * Copyright 2015 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.
+ */
+
+'use strict';
+
+var arrify = require('arrify');
+var assert = require('assert');
+var extend = require('extend');
+var mockery = require('mockery');
+
+function FakeAddress() {
+ this.calledWith_ = [].slice.call(arguments);
+}
+
+function FakeOperation() {
+ this.calledWith_ = [].slice.call(arguments);
+}
+
+var extended = false;
+var fakeStreamRouter = {
+ extend: function(Class, methods) {
+ if (Class.name !== 'Region') {
+ return;
+ }
+
+ extended = true;
+ methods = arrify(methods);
+ assert.equal(Class.name, 'Region');
+ assert.deepEqual(methods, ['getAddresses', 'getOperations']);
+ }
+};
+
+describe('Region', function() {
+ var Region;
+ var region;
+
+ var COMPUTE = {
+ authConfig: { a: 'b', c: 'd' }
+ };
+ var REGION_NAME = 'us-central1';
+
+ before(function() {
+ mockery.registerMock('../common/stream-router.js', fakeStreamRouter);
+ mockery.registerMock('./address.js', FakeAddress);
+ mockery.registerMock('./operation.js', FakeOperation);
+
+ mockery.enable({
+ useCleanCache: true,
+ warnOnUnregistered: false
+ });
+
+ Region = require('../../lib/compute/region.js');
+ });
+
+ after(function() {
+ mockery.deregisterAll();
+ mockery.disable();
+ });
+
+ beforeEach(function() {
+ region = new Region(COMPUTE, REGION_NAME);
+ });
+
+ describe('instantiation', function() {
+ it('should extend the correct methods', function() {
+ assert(extended); // See `fakeStreamRouter.extend`
+ });
+
+ it('should localize the compute instance', function() {
+ assert.strictEqual(region.compute, COMPUTE);
+ });
+
+ it('should localize the name', function() {
+ assert.strictEqual(region.name, REGION_NAME);
+ });
+
+ it('should default metadata to an empty object', function() {
+ assert.strictEqual(typeof region.metadata, 'object');
+ assert.strictEqual(Object.keys(region.metadata).length, 0);
+ });
+ });
+
+ describe('address', function() {
+ var NAME = 'address-name';
+
+ it('should return an Address object', function() {
+ var address = region.address(NAME);
+ assert(address instanceof FakeAddress);
+ assert.strictEqual(address.calledWith_[0], region);
+ assert.strictEqual(address.calledWith_[1], NAME);
+ });
+ });
+
+ describe('createAddress', function() {
+ var NAME = 'address-name';
+ var OPTIONS = { a: 'b', c: 'd' };
+ var EXPECTED_BODY = extend({}, OPTIONS, { name: NAME });
+
+ it('should not require any options', function(done) {
+ var expectedBody = { name: NAME };
+
+ region.makeReq_ = function(method, path, query, body) {
+ assert.deepEqual(body, expectedBody);
+ done();
+ };
+
+ region.createAddress(NAME, assert.ifError);
+ });
+
+ it('should make the correct API request', function(done) {
+ region.makeReq_ = function(method, path, query, body) {
+ assert.strictEqual(method, 'POST');
+ assert.strictEqual(path, '/addresses');
+ assert.strictEqual(query, null);
+ assert.deepEqual(body, EXPECTED_BODY);
+
+ done();
+ };
+
+ region.createAddress(NAME, OPTIONS, assert.ifError);
+ });
+
+ describe('error', function() {
+ var error = new Error('Error.');
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ region.makeReq_ = function(method, path, query, body, callback) {
+ callback(error, apiResponse);
+ };
+ });
+
+ it('should execute callback with error & API response', function(done) {
+ region.createAddress(NAME, OPTIONS, function(err, address_, op, resp) {
+ assert.strictEqual(err, error);
+ assert.strictEqual(address_, null);
+ assert.strictEqual(op, null);
+ assert.strictEqual(resp, apiResponse);
+ done();
+ });
+ });
+ });
+
+ describe('success', function() {
+ var apiResponse = { name: 'operation-name' };
+
+ beforeEach(function() {
+ region.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponse);
+ };
+ });
+
+ it('should exec callback with Address, Op & apiResponse', function(done) {
+ var address = {};
+ var operation = {};
+
+ region.address = function(name) {
+ assert.strictEqual(name, NAME);
+ return address;
+ };
+
+ region.operation = function(name) {
+ assert.strictEqual(name, apiResponse.name);
+ return operation;
+ };
+
+ region.createAddress(NAME, OPTIONS, function(err, address_, op, resp) {
+ assert.ifError(err);
+
+ assert.strictEqual(address_, address);
+
+ assert.strictEqual(op, operation);
+ assert.strictEqual(op.metadata, resp);
+
+ assert.strictEqual(resp, apiResponse);
+ done();
+ });
+ });
+ });
+ });
+
+ describe('getAddresses', function() {
+ it('should accept only a callback', function(done) {
+ region.makeReq_ = function(method, path, query) {
+ assert.deepEqual(query, {});
+ done();
+ };
+
+ region.getAddresses(assert.ifError);
+ });
+
+ it('should make the correct API request', function(done) {
+ var query = { a: 'b', c: 'd' };
+
+ region.makeReq_ = function(method, path, query_, body) {
+ assert.strictEqual(method, 'GET');
+ assert.strictEqual(path, '/addresses');
+ assert.strictEqual(query_, query);
+ assert.strictEqual(body, null);
+
+ done();
+ };
+
+ region.getAddresses(query, assert.ifError);
+ });
+
+ describe('error', function() {
+ var error = new Error('Error.');
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ region.makeReq_ = function(method, path, query, body, callback) {
+ callback(error, apiResponse);
+ };
+ });
+
+ it('should execute callback with error & API response', function(done) {
+ region.getAddresses({}, function(err, addresses, nextQuery, apiResp) {
+ assert.strictEqual(err, error);
+ assert.strictEqual(addresses, null);
+ assert.strictEqual(nextQuery, null);
+ assert.strictEqual(apiResp, apiResponse);
+ done();
+ });
+ });
+ });
+
+ describe('success', function() {
+ var apiResponse = {
+ items: [
+ { name: 'operation-name' }
+ ]
+ };
+
+ beforeEach(function() {
+ region.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponse);
+ };
+ });
+
+ it('should build a nextQuery if necessary', function(done) {
+ var nextPageToken = 'next-page-token';
+ var apiResponseWithNextPageToken = extend({}, apiResponse, {
+ nextPageToken: nextPageToken
+ });
+ var expectedNextQuery = {
+ pageToken: nextPageToken
+ };
+
+ region.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponseWithNextPageToken);
+ };
+
+ region.getAddresses({}, function(err, addresses, nextQuery) {
+ assert.ifError(err);
+
+ assert.deepEqual(nextQuery, expectedNextQuery);
+
+ done();
+ });
+ });
+
+ it('should execute callback with Operations & API resp', function(done) {
+ var address = {};
+
+ region.address = function(name) {
+ assert.strictEqual(name, apiResponse.items[0].name);
+ return address;
+ };
+
+ region.getAddresses({}, function(err, addresses, nextQuery, apiResp) {
+ assert.ifError(err);
+
+ assert.strictEqual(addresses[0], address);
+ assert.strictEqual(addresses[0].metadata, apiResponse.items[0]);
+
+ assert.strictEqual(apiResp, apiResponse);
+
+ done();
+ });
+ });
+ });
+ });
+
+ describe('getMetadata', function() {
+ it('should make the correct API request', function(done) {
+ region.makeReq_ = function(method, path, query, body) {
+ assert.strictEqual(method, 'GET');
+ assert.strictEqual(path, '');
+ assert.strictEqual(query, null);
+ assert.strictEqual(body, null);
+
+ done();
+ };
+
+ region.getMetadata(assert.ifError);
+ });
+
+ describe('error', function() {
+ var error = new Error('Error.');
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ region.makeReq_ = function(method, path, query, body, callback) {
+ callback(error, apiResponse);
+ };
+ });
+
+ it('should execute callback with error and API response', function(done) {
+ region.getMetadata(function(err, metadata, apiResponse_) {
+ assert.strictEqual(err, error);
+ assert.strictEqual(metadata, null);
+ assert.strictEqual(apiResponse_, apiResponse);
+ done();
+ });
+ });
+
+ it('should not require a callback', function() {
+ assert.doesNotThrow(function() {
+ region.getMetadata();
+ });
+ });
+ });
+
+ describe('success', function() {
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ region.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponse);
+ };
+ });
+
+ it('should update the metadata to the API response', function(done) {
+ region.getMetadata(function(err) {
+ assert.ifError(err);
+ assert.strictEqual(region.metadata, apiResponse);
+ done();
+ });
+ });
+
+ it('should exec callback with metadata and API response', function(done) {
+ region.getMetadata(function(err, metadata, apiResponse_) {
+ assert.ifError(err);
+ assert.strictEqual(metadata, apiResponse);
+ assert.strictEqual(apiResponse_, apiResponse);
+ done();
+ });
+ });
+
+ it('should not require a callback', function() {
+ assert.doesNotThrow(function() {
+ region.getMetadata();
+ });
+ });
+ });
+ });
+
+ describe('getOperations', function() {
+ it('should accept only a callback', function(done) {
+ region.makeReq_ = function(method, path, query) {
+ assert.deepEqual(query, {});
+ done();
+ };
+
+ region.getOperations(assert.ifError);
+ });
+
+ it('should make the correct API request', function(done) {
+ var query = { a: 'b', c: 'd' };
+
+ region.makeReq_ = function(method, path, query_, body) {
+ assert.strictEqual(method, 'GET');
+ assert.strictEqual(path, '/operations');
+ assert.strictEqual(query_, query);
+ assert.strictEqual(body, null);
+
+ done();
+ };
+
+ region.getOperations(query, assert.ifError);
+ });
+
+ describe('error', function() {
+ var error = new Error('Error.');
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ region.makeReq_ = function(method, path, query, body, callback) {
+ callback(error, apiResponse);
+ };
+ });
+
+ it('should execute callback with error & API response', function(done) {
+ region.getOperations({}, function(err, operations, nextQuery, apiResp) {
+ assert.strictEqual(err, error);
+ assert.strictEqual(operations, null);
+ assert.strictEqual(nextQuery, null);
+ assert.strictEqual(apiResp, apiResponse);
+ done();
+ });
+ });
+ });
+
+ describe('success', function() {
+ var apiResponse = {
+ items: [
+ { name: 'operation-name' }
+ ]
+ };
+
+ beforeEach(function() {
+ region.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponse);
+ };
+ });
+
+ it('should build a nextQuery if necessary', function(done) {
+ var nextPageToken = 'next-page-token';
+ var apiResponseWithNextPageToken = extend({}, apiResponse, {
+ nextPageToken: nextPageToken
+ });
+ var expectedNextQuery = {
+ pageToken: nextPageToken
+ };
+
+ region.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponseWithNextPageToken);
+ };
+
+ region.getOperations({}, function(err, operations, nextQuery) {
+ assert.ifError(err);
+
+ assert.deepEqual(nextQuery, expectedNextQuery);
+
+ done();
+ });
+ });
+
+ it('should execute callback with Operations & API resp', function(done) {
+ var operation = {};
+
+ region.operation = function(name) {
+ assert.strictEqual(name, apiResponse.items[0].name);
+ return operation;
+ };
+
+ region.getOperations({}, function(err, operations, nextQuery, apiResp) {
+ assert.ifError(err);
+
+ assert.strictEqual(operations[0], operation);
+ assert.strictEqual(operations[0].metadata, apiResponse.items[0]);
+
+ assert.strictEqual(apiResp, apiResponse);
+
+ done();
+ });
+ });
+ });
+ });
+
+ describe('operation', function() {
+ var NAME = 'operation-name';
+
+ it('should return a Operation object', function() {
+ var operation = region.operation(NAME);
+ assert(operation instanceof FakeOperation);
+ assert.strictEqual(operation.calledWith_[0], region);
+ assert.strictEqual(operation.calledWith_[1], NAME);
+ });
+ });
+
+ describe('makeReq_', function() {
+ it('should make the correct request to Compute', function(done) {
+ var expectedPathPrefix = '/regions/' + region.name;
+
+ var method = 'POST';
+ var path = '/test';
+ var query = {
+ a: 'b',
+ c: 'd'
+ };
+ var body = {
+ a: 'b',
+ c: 'd'
+ };
+
+ region.compute.makeReq_ = function(method_, path_, query_, body_, cb) {
+ assert.strictEqual(method_, method);
+ assert.strictEqual(path_, expectedPathPrefix + path);
+ assert.strictEqual(query_, query);
+ assert.strictEqual(body_, body);
+ cb();
+ };
+
+ region.makeReq_(method, path, query, body, done);
+ });
+ });
+});
diff --git a/test/compute/snapshot.js b/test/compute/snapshot.js
new file mode 100644
index 00000000000..6dc6dbb9ecc
--- /dev/null
+++ b/test/compute/snapshot.js
@@ -0,0 +1,223 @@
+/**
+ * Copyright 2015 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.
+ */
+
+'use strict';
+
+var assert = require('assert');
+var Snapshot = require('../../lib/compute/snapshot.js');
+
+describe('Snapshot', function() {
+ var COMPUTE = {};
+ var SNAPSHOT_NAME = 'snapshot-name';
+
+ var snapshot;
+
+ beforeEach(function() {
+ snapshot = new Snapshot(COMPUTE, SNAPSHOT_NAME);
+ });
+
+ describe('instantiation', function() {
+ it('should localize the compute instance', function() {
+ assert.strictEqual(snapshot.compute, COMPUTE);
+ });
+
+ it('should localize the name', function() {
+ assert.strictEqual(snapshot.name, SNAPSHOT_NAME);
+ });
+
+ it('should default metadata to an empty object', function() {
+ assert.strictEqual(typeof snapshot.metadata, 'object');
+ assert.strictEqual(Object.keys(snapshot.metadata).length, 0);
+ });
+ });
+
+ describe('delete', function() {
+ it('should make the correct API request', function(done) {
+ snapshot.makeReq_ = function(method, path, query, body) {
+ assert.strictEqual(method, 'DELETE');
+ assert.strictEqual(path, '');
+ assert.strictEqual(query, null);
+ assert.strictEqual(body, null);
+
+ done();
+ };
+
+ snapshot.delete(assert.ifError);
+ });
+
+ describe('error', function() {
+ var error = new Error('Error.');
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ snapshot.makeReq_ = function(method, path, query, body, callback) {
+ callback(error, apiResponse);
+ };
+ });
+
+ it('should exec the callback with error & API response', function(done) {
+ snapshot.delete(function(err, operation, apiResponse_) {
+ assert.strictEqual(err, error);
+ assert.strictEqual(operation, null);
+ assert.strictEqual(apiResponse_, apiResponse);
+ done();
+ });
+ });
+
+ it('should not require a callback', function() {
+ assert.doesNotThrow(function() {
+ snapshot.delete();
+ });
+ });
+ });
+
+ describe('success', function() {
+ var apiResponse = { name: 'operation-name' };
+
+ beforeEach(function() {
+ snapshot.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponse);
+ };
+ });
+
+ it('should exec callback with Operation & API response', function(done) {
+ var operation = {};
+
+ snapshot.compute.operation = function(name) {
+ assert.strictEqual(name, apiResponse.name);
+ return operation;
+ };
+
+ snapshot.delete(function(err, operation_, apiResponse_) {
+ assert.ifError(err);
+
+ assert.strictEqual(operation_, operation);
+ assert.strictEqual(operation_.metadata, apiResponse);
+
+ assert.strictEqual(apiResponse_, apiResponse);
+ done();
+ });
+ });
+
+ it('should not require a callback', function() {
+ assert.doesNotThrow(function() {
+ snapshot.delete();
+ });
+ });
+ });
+ });
+
+ describe('getMetadata', function() {
+ it('should make the correct API request', function(done) {
+ snapshot.makeReq_ = function(method, path, query, body) {
+ assert.strictEqual(method, 'GET');
+ assert.strictEqual(path, '');
+ assert.strictEqual(query, null);
+ assert.strictEqual(body, null);
+
+ done();
+ };
+
+ snapshot.getMetadata(assert.ifError);
+ });
+
+ describe('error', function() {
+ var error = new Error('Error.');
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ snapshot.makeReq_ = function(method, path, query, body, callback) {
+ callback(error, apiResponse);
+ };
+ });
+
+ it('should execute callback with error and API response', function(done) {
+ snapshot.getMetadata(function(err, metadata, apiResponse_) {
+ assert.strictEqual(err, error);
+ assert.strictEqual(metadata, null);
+ assert.strictEqual(apiResponse_, apiResponse);
+ done();
+ });
+ });
+
+ it('should not require a callback', function() {
+ assert.doesNotThrow(function() {
+ snapshot.getMetadata();
+ });
+ });
+ });
+
+ describe('success', function() {
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ snapshot.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponse);
+ };
+ });
+
+ it('should update the metadata to the API response', function(done) {
+ snapshot.getMetadata(function(err) {
+ assert.ifError(err);
+ assert.strictEqual(snapshot.metadata, apiResponse);
+ done();
+ });
+ });
+
+ it('should exec callback with metadata and API response', function(done) {
+ snapshot.getMetadata(function(err, metadata, apiResponse_) {
+ assert.ifError(err);
+ assert.strictEqual(metadata, apiResponse);
+ assert.strictEqual(apiResponse_, apiResponse);
+ done();
+ });
+ });
+
+ it('should not require a callback', function() {
+ assert.doesNotThrow(function() {
+ snapshot.getMetadata();
+ });
+ });
+ });
+ });
+
+ describe('makeReq_', function() {
+ it('should make the correct request to Compute', function(done) {
+ var expectedPathPrefix = '/global/snapshots/' + snapshot.name;
+
+ var method = 'POST';
+ var path = '/test';
+ var query = {
+ a: 'b',
+ c: 'd'
+ };
+ var body = {
+ a: 'b',
+ c: 'd'
+ };
+
+ snapshot.compute.makeReq_ = function(method_, path_, query_, body_, cb) {
+ assert.strictEqual(method_, method);
+ assert.strictEqual(path_, expectedPathPrefix + path);
+ assert.strictEqual(query_, query);
+ assert.strictEqual(body_, body);
+ cb();
+ };
+
+ snapshot.makeReq_(method, path, query, body, done);
+ });
+ });
+});
diff --git a/test/compute/vm.js b/test/compute/vm.js
new file mode 100644
index 00000000000..fa8792a3089
--- /dev/null
+++ b/test/compute/vm.js
@@ -0,0 +1,538 @@
+/**
+ * Copyright 2015 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.
+ */
+
+'use strict';
+
+var assert = require('assert');
+var extend = require('extend');
+
+var Disk = require('../../lib/compute/disk.js');
+var util = require('../../lib/common/util.js');
+var VM = require('../../lib/compute/vm.js');
+
+describe('VM', function() {
+ var vm;
+
+ var COMPUTE = { projectId: 'project-id' };
+ var ZONE = { compute: COMPUTE, name: 'us-central1-a' };
+ var VM_NAME = 'vm-name';
+
+ var DISK = new Disk(ZONE, 'disk-name');
+
+ beforeEach(function() {
+ vm = new VM(ZONE, VM_NAME);
+ });
+
+ describe('instantiation', function() {
+ it('should localize the zone', function() {
+ assert.strictEqual(vm.zone, ZONE);
+ });
+
+ it('should localize the name', function() {
+ assert.strictEqual(vm.name, VM_NAME);
+ });
+ });
+
+ describe('attachDisk', function() {
+ var CONFIG = {};
+ var EXPECTED_BODY = { source: DISK.formattedName };
+
+ it('should throw if a Disk object is not provided', function() {
+ assert.throws(function() {
+ vm.attachDisk('disk-3', CONFIG, assert.ifError);
+ }, /A Disk object must be provided/);
+
+ assert.doesNotThrow(function() {
+ vm.makeReq_ = util.noop;
+ vm.attachDisk(DISK, CONFIG, assert.ifError);
+ });
+ });
+
+ it('should not require an options object', function(done) {
+ vm.makeReq_ = function(method, path, query, body) {
+ assert.deepEqual(body, EXPECTED_BODY);
+ done();
+ };
+
+ vm.attachDisk(DISK, assert.ifError);
+ });
+
+ describe('options.readOnly', function() {
+ var CONFIG = extend({}, CONFIG, { readOnly: true });
+
+ it('should set the correct mode', function(done) {
+ var expectedBody = extend({}, EXPECTED_BODY, { mode: 'READ_ONLY' });
+
+ vm.makeReq_ = function(method, path, query, body) {
+ assert.deepEqual(body, expectedBody);
+ done();
+ };
+
+ vm.attachDisk(DISK, CONFIG, assert.ifError);
+ });
+
+ it('should delete the readOnly property', function(done) {
+ vm.makeReq_ = function(method, path, query, body) {
+ assert.strictEqual(typeof body.readOnly, 'undefined');
+ done();
+ };
+
+ vm.attachDisk(DISK, CONFIG, assert.ifError);
+ });
+ });
+
+ it('should make the correct API request', function(done) {
+ vm.makeReq_ = function(method, path, query, body, callback) {
+ assert.strictEqual(method, 'POST');
+ assert.strictEqual(path, '/attachDisk');
+ assert.strictEqual(query, null);
+ assert.deepEqual(body, EXPECTED_BODY);
+
+ callback();
+ };
+
+ vm.attachDisk(DISK, CONFIG, done);
+ });
+ });
+
+ describe('delete', function() {
+ it('should make the correct API request', function(done) {
+ vm.makeReq_ = function(method, path, query, body, callback) {
+ assert.strictEqual(method, 'DELETE');
+ assert.strictEqual(path, '');
+ assert.strictEqual(query, null);
+ assert.strictEqual(body, null);
+
+ callback();
+ };
+
+ vm.delete(done);
+ });
+
+ it('should not require a callback', function(done) {
+ vm.makeReq_ = function(method, path, query, body, callback) {
+ assert.doesNotThrow(function() {
+ callback();
+ done();
+ });
+ };
+
+ vm.delete();
+ });
+ });
+
+ describe('detachDisk', function() {
+ it('should throw if a Disk is not provided', function() {
+ assert.throws(function() {
+ vm.detachDisk('disk-name');
+ }, /A Disk object must be provided/);
+
+ assert.doesNotThrow(function() {
+ vm.makeReq_ = util.noop;
+ vm.detachDisk(DISK);
+ });
+ });
+
+ it('should make the correct API request', function(done) {
+ vm.makeReq_ = function(method, path, query, body, callback) {
+ assert.strictEqual(method, 'POST');
+ assert.strictEqual(path, '/detachDisk');
+ assert.deepEqual(query, { deviceName: DISK.name });
+ assert.strictEqual(body, null);
+
+ callback();
+ };
+
+ vm.detachDisk(DISK, done);
+ });
+
+ it('should not require a callback', function(done) {
+ vm.makeReq_ = function(method, path, query, body, callback) {
+ assert.doesNotThrow(function() {
+ callback();
+ done();
+ });
+ };
+
+ vm.detachDisk(DISK);
+ });
+ });
+
+ describe('getMetadata', function() {
+ it('should make the correct API request', function(done) {
+ vm.makeReq_ = function(method, path, query, body) {
+ assert.strictEqual(method, 'GET');
+ assert.strictEqual(path, '');
+ assert.strictEqual(query, null);
+ assert.strictEqual(body, null);
+
+ done();
+ };
+
+ vm.getMetadata(assert.ifError);
+ });
+
+ describe('error', function() {
+ var error = new Error('Error.');
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ vm.makeReq_ = function(method, path, query, body, callback) {
+ callback(error, null/*usually an operation*/, apiResponse);
+ };
+ });
+
+ it('should execute callback with error and API response', function(done) {
+ vm.getMetadata(function(err, metadata, apiResponse_) {
+ assert.strictEqual(err, error);
+ assert.strictEqual(metadata, null);
+ assert.strictEqual(apiResponse_, apiResponse);
+ done();
+ });
+ });
+
+ it('should not require a callback', function() {
+ assert.doesNotThrow(function() {
+ vm.getMetadata();
+ });
+ });
+ });
+
+ describe('success', function() {
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ vm.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, null/*usually an operation*/, apiResponse);
+ };
+ });
+
+ it('should update the metadata to the API response', function(done) {
+ vm.getMetadata(function(err) {
+ assert.ifError(err);
+ assert.strictEqual(vm.metadata, apiResponse);
+ done();
+ });
+ });
+
+ it('should exec callback with metadata and API response', function(done) {
+ vm.getMetadata(function(err, metadata, apiResponse_) {
+ assert.ifError(err);
+ assert.strictEqual(metadata, apiResponse);
+ assert.strictEqual(apiResponse_, apiResponse);
+ done();
+ });
+ });
+
+ it('should not require a callback', function() {
+ assert.doesNotThrow(function() {
+ vm.getMetadata();
+ });
+ });
+ });
+ });
+
+ describe('getSerialPortOutput', function() {
+ var EXPECTED_QUERY = { port: 1 };
+
+ it('should default to port 1', function(done) {
+ vm.makeReq_ = function(method, path, query) {
+ assert.strictEqual(query.port, 1);
+ done();
+ };
+
+ vm.getSerialPortOutput(assert.ifError);
+ });
+
+ it('should override the default port', function(done) {
+ var port = 8001;
+
+ vm.makeReq_ = function(method, path, query) {
+ assert.strictEqual(query.port, port);
+ done();
+ };
+
+ vm.getSerialPortOutput(port, assert.ifError);
+ });
+
+ it('should make the correct API request', function(done) {
+ vm.makeReq_ = function(method, path, query, body) {
+ assert.strictEqual(method, 'GET');
+ assert.strictEqual(path, '/serialPort');
+ assert.deepEqual(query, EXPECTED_QUERY);
+ assert.strictEqual(body, null);
+
+ done();
+ };
+
+ vm.getSerialPortOutput(assert.ifError);
+ });
+
+ describe('error', function() {
+ var error = new Error('Error.');
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ vm.makeReq_ = function(method, path, query, body, callback) {
+ callback(error, null/*usually an operation*/, apiResponse);
+ };
+ });
+
+ it('should execute callback with error & API response', function(done) {
+ vm.getSerialPortOutput(function(err, output, apiResponse_) {
+ assert.strictEqual(err, error);
+ assert.strictEqual(output, null);
+ assert.strictEqual(apiResponse_, apiResponse);
+
+ done();
+ });
+ });
+ });
+
+ describe('success', function() {
+ var apiResponse = { contents: 'contents' };
+
+ beforeEach(function() {
+ vm.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, null/*usually an operation*/, apiResponse);
+ };
+ });
+
+ it('should exec callback with contents & API response', function(done) {
+ vm.getSerialPortOutput(function(err, output, apiResponse_) {
+ assert.ifError(err);
+ assert.strictEqual(output, apiResponse.contents);
+ assert.strictEqual(apiResponse_, apiResponse);
+
+ done();
+ });
+ });
+ });
+ });
+
+ describe('getTags', function() {
+ it('should get metadata', function(done) {
+ vm.getMetadata = function() {
+ done();
+ };
+
+ vm.getTags(assert.ifError);
+ });
+
+ describe('error', function() {
+ var error = new Error('Error.');
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ vm.getMetadata = function(callback) {
+ callback(error, null, apiResponse);
+ };
+ });
+
+ it('should execute callback with error', function(done) {
+ vm.getTags(function(err, tags, fingerprint, apiResponse_) {
+ assert.strictEqual(err, error);
+ assert.strictEqual(tags, null);
+ assert.strictEqual(fingerprint, null);
+ assert.strictEqual(apiResponse_, apiResponse);
+
+ done();
+ });
+ });
+ });
+
+ describe('success', function() {
+ var metadata = {
+ tags: {
+ items: [],
+ fingerprint: 'fingerprint'
+ }
+ };
+
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ vm.getMetadata = function(callback) {
+ callback(null, metadata, apiResponse);
+ };
+ });
+
+ it('should execute callback with tags and fingerprint', function(done) {
+ vm.getTags(function(err, tags, fingerprint, apiResponse_) {
+ assert.ifError(err);
+
+ assert.strictEqual(tags, metadata.tags.items);
+ assert.strictEqual(fingerprint, metadata.tags.fingerprint);
+ assert.strictEqual(apiResponse_, apiResponse);
+
+ done();
+ });
+ });
+ });
+ });
+
+ describe('reset', function() {
+ it('should make the correct API request', function(done) {
+ vm.makeReq_ = function(method, path, query, body, callback) {
+ assert.strictEqual(method, 'POST');
+ assert.strictEqual(path, '/reset');
+ assert.strictEqual(query, null);
+ assert.strictEqual(body, null);
+
+ callback();
+ };
+
+ vm.reset(done);
+ });
+
+ it('should not require a callback', function(done) {
+ vm.makeReq_ = function(method, path, query, body, callback) {
+ assert.doesNotThrow(function() {
+ callback();
+ done();
+ });
+ };
+
+ vm.reset();
+ });
+ });
+
+ describe('start', function() {
+ it('should make the correct API request', function(done) {
+ vm.makeReq_ = function(method, path, query, body, callback) {
+ assert.strictEqual(method, 'POST');
+ assert.strictEqual(path, '/start');
+ assert.strictEqual(query, null);
+ assert.strictEqual(body, null);
+
+ callback();
+ };
+
+ vm.start(done);
+ });
+
+ it('should not require a callback', function(done) {
+ vm.makeReq_ = function(method, path, query, body, callback) {
+ assert.doesNotThrow(function() {
+ callback();
+ done();
+ });
+ };
+
+ vm.start();
+ });
+ });
+
+ describe('stop', function() {
+ it('should make the correct API request', function(done) {
+ vm.makeReq_ = function(method, path, query, body, callback) {
+ assert.strictEqual(method, 'POST');
+ assert.strictEqual(path, '/stop');
+ assert.strictEqual(query, null);
+ assert.strictEqual(body, null);
+
+ callback();
+ };
+
+ vm.stop(done);
+ });
+
+ it('should not require a callback', function(done) {
+ vm.makeReq_ = function(method, path, query, body, callback) {
+ assert.doesNotThrow(function() {
+ callback();
+ done();
+ });
+ };
+
+ vm.stop();
+ });
+ });
+
+ describe('makeReq_', function() {
+ it('should make the correct request to Compute', function(done) {
+ var expectedPathPrefix = '/instances/' + vm.name;
+
+ var method = 'POST';
+ var path = '/test';
+ var query = {
+ a: 'b',
+ c: 'd'
+ };
+ var body = {
+ a: 'b',
+ c: 'd'
+ };
+
+ vm.zone.makeReq_ = function(method_, path_, query_, body_) {
+ assert.strictEqual(method_, method);
+ assert.strictEqual(path_, expectedPathPrefix + path);
+ assert.strictEqual(query_, query);
+ assert.strictEqual(body_, body);
+
+ done();
+ };
+
+ vm.makeReq_(method, path, query, body, assert.ifError);
+ });
+ });
+
+ describe('error', function() {
+ var error = new Error('Error.');
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ vm.zone.makeReq_ = function(method, path, query, body, callback) {
+ callback(error, apiResponse);
+ };
+ });
+
+ it('should execute callback with error & API response', function(done) {
+ vm.makeReq_('POST', '/', {}, {}, function(err, operation, resp) {
+ assert.strictEqual(err, error);
+ assert.strictEqual(operation, null);
+ assert.strictEqual(resp, apiResponse);
+ done();
+ });
+ });
+ });
+
+ describe('success', function() {
+ var apiResponse = { name: 'operation-name' };
+
+ beforeEach(function() {
+ vm.zone.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponse);
+ };
+ });
+
+ it('should execute callback with a Zone object & API resp', function(done) {
+ var operation = {};
+
+ vm.zone.operation = function(name) {
+ assert.strictEqual(name, apiResponse.name);
+ return operation;
+ };
+
+ vm.makeReq_('POST', '/', {}, {}, function(err, operation_, resp) {
+ assert.ifError(err);
+ assert.strictEqual(operation_, operation);
+ assert.strictEqual(resp, apiResponse);
+ done();
+ });
+ });
+ });
+});
diff --git a/test/compute/zone.js b/test/compute/zone.js
new file mode 100644
index 00000000000..684342d2ce2
--- /dev/null
+++ b/test/compute/zone.js
@@ -0,0 +1,1004 @@
+/**
+ * Copyright 2015 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.
+ */
+
+'use strict';
+
+var arrify = require('arrify');
+var assert = require('assert');
+var extend = require('extend');
+var gceImages = require('gce-images');
+var mockery = require('mockery');
+
+var util = require('../../lib/common/util.js');
+
+var gceImagesOverride = null;
+function fakeGceImages() {
+ return (gceImagesOverride || gceImages).apply(null, arguments);
+}
+
+function FakeDisk() {
+ this.calledWith_ = [].slice.call(arguments);
+}
+
+function FakeOperation() {
+ this.calledWith_ = [].slice.call(arguments);
+}
+
+function FakeVM() {
+ this.calledWith_ = [].slice.call(arguments);
+}
+
+var extended = false;
+var fakeStreamRouter = {
+ extend: function(Class, methods) {
+ if (Class.name !== 'Zone') {
+ return;
+ }
+
+ extended = true;
+ methods = arrify(methods);
+ assert.equal(Class.name, 'Zone');
+ assert.deepEqual(methods, ['getDisks', 'getOperations', 'getVMs']);
+ }
+};
+
+describe('Zone', function() {
+ var Zone;
+ var zone;
+
+ var COMPUTE = {
+ authConfig: { a: 'b', c: 'd' }
+ };
+ var ZONE_NAME = 'us-central1-a';
+
+ before(function() {
+ mockery.registerMock('gce-images', fakeGceImages);
+ mockery.registerMock('../common/stream-router.js', fakeStreamRouter);
+ mockery.registerMock('./disk.js', FakeDisk);
+ mockery.registerMock('./operation.js', FakeOperation);
+ mockery.registerMock('./vm.js', FakeVM);
+
+ mockery.enable({
+ useCleanCache: true,
+ warnOnUnregistered: false
+ });
+
+ Zone = require('../../lib/compute/zone.js');
+ });
+
+ after(function() {
+ mockery.deregisterAll();
+ mockery.disable();
+ });
+
+ beforeEach(function() {
+ gceImagesOverride = null;
+ zone = new Zone(COMPUTE, ZONE_NAME);
+ });
+
+ describe('instantiation', function() {
+ it('should extend the correct methods', function() {
+ assert(extended); // See `fakeStreamRouter.extend`
+ });
+
+ it('should localize the compute instance', function() {
+ assert.strictEqual(zone.compute, COMPUTE);
+ });
+
+ it('should localize the name', function() {
+ assert.strictEqual(zone.name, ZONE_NAME);
+ });
+
+ it('should default metadata to an empty object', function() {
+ assert.strictEqual(typeof zone.metadata, 'object');
+ assert.strictEqual(Object.keys(zone.metadata).length, 0);
+ });
+
+ it('should create a gceImages instance', function() {
+ var gceVal = 'ok';
+
+ gceImagesOverride = function(authConfig) {
+ assert.strictEqual(authConfig, COMPUTE.authConfig);
+ return gceVal;
+ };
+
+ var newZone = new Zone(COMPUTE, ZONE_NAME);
+ assert.strictEqual(newZone.gceImages, gceVal);
+ });
+ });
+
+ describe('createDisk', function() {
+ var NAME = 'disk-name';
+
+ beforeEach(function() {
+ zone.makeReq_ = util.noop;
+ });
+
+ it('should use the image property as qs.sourceImages', function(done) {
+ var config = {
+ image: 'abc'
+ };
+
+ zone.makeReq_ = function(method, path, query) {
+ assert.strictEqual(query.sourceImage, config.image);
+ done();
+ };
+
+ zone.createDisk(NAME, config, assert.ifError);
+ });
+
+ describe('config.os', function() {
+ var CONFIG = {
+ os: 'os-name'
+ };
+
+ it('should get the latest image', function(done) {
+ zone.gceImages.getLatest = function(os) {
+ assert.strictEqual(os, CONFIG.os);
+ done();
+ };
+
+ zone.createDisk(NAME, CONFIG, assert.ifError);
+ });
+
+ describe('error', function() {
+ var error = new Error('Error.');
+
+ beforeEach(function() {
+ zone.gceImages.getLatest = function(os, callback) {
+ callback(error);
+ };
+ });
+
+ it('should execute callback with error', function(done) {
+ zone.createDisk(NAME, CONFIG, function(err) {
+ assert.strictEqual(err, error);
+ done();
+ });
+ });
+ });
+
+ describe('success', function() {
+ var gceImagesResp = {
+ selfLink: 'http://selflink'
+ };
+
+ var expectedConfig = {
+ name: NAME,
+ sourceImage: gceImagesResp.selfLink
+ };
+
+ it('should call createDisk with the correct config', function(done) {
+ zone.gceImages.getLatest = function(os, callback) {
+ zone.createDisk = function(name, config, callback) {
+ assert.strictEqual(name, NAME);
+ assert.deepEqual(config, expectedConfig);
+ callback();
+ };
+
+ callback(null, gceImagesResp);
+ };
+
+ zone.createDisk(NAME, CONFIG, done);
+ });
+ });
+ });
+
+ describe('API request', function() {
+ var CONFIG = {
+ a: 'b',
+ c: 'd'
+ };
+
+ var expectedBody = {
+ name: NAME,
+ a: 'b',
+ c: 'd'
+ };
+
+ it('should make the correct API request', function(done) {
+ zone.makeReq_ = function(method, path, query, body) {
+ assert.strictEqual(method, 'POST');
+ assert.strictEqual(path, '/disks');
+ assert.deepEqual(query, {});
+ assert.deepEqual(body, expectedBody);
+
+ done();
+ };
+
+ zone.createDisk(NAME, CONFIG, assert.ifError);
+ });
+
+ describe('error', function() {
+ var error = new Error('Error.');
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ zone.makeReq_ = function(method, path, query, body, callback) {
+ callback(error, apiResponse);
+ };
+ });
+
+ it('should execute callback with error & API response', function(done) {
+ zone.createDisk(NAME, CONFIG, function(err, disk, op, apiResp) {
+ assert.strictEqual(err, error);
+ assert.strictEqual(disk, null);
+ assert.strictEqual(op, null);
+ assert.strictEqual(apiResp, apiResponse);
+ done();
+ });
+ });
+ });
+
+ describe('success', function() {
+ var apiResponse = { name: 'operation-name' };
+
+ beforeEach(function() {
+ zone.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponse);
+ };
+ });
+
+ it('should exec callback with Disk, Op & apiResponse', function(done) {
+ var disk = {};
+ var operation = {};
+
+ zone.disk = function(name) {
+ assert.strictEqual(name, NAME);
+ return disk;
+ };
+
+ zone.operation = function(name) {
+ assert.strictEqual(name, apiResponse.name);
+ return operation;
+ };
+
+ zone.createDisk(NAME, CONFIG, function(err, disk_, op, apiResp) {
+ assert.ifError(err);
+
+ assert.strictEqual(disk_, disk);
+
+ assert.strictEqual(op, operation);
+ assert.strictEqual(op.metadata, apiResp);
+
+ assert.strictEqual(apiResp, apiResponse);
+ done();
+ });
+ });
+ });
+ });
+ });
+
+ describe('createVM', function() {
+ var NAME = 'new-vm';
+
+ var CONFIG = {};
+
+ var EXPECTED_CONFIG = {
+ name: NAME,
+ machineType: 'zones/' + ZONE_NAME + '/machineTypes/n1-standard-1',
+ networkInterfaces: [
+ {
+ network: 'global/networks/default'
+ }
+ ]
+ };
+
+ describe('config.machineType', function() {
+ var CONFIG = {
+ machineType: 'f1-micro'
+ };
+
+ it('should format a given machine type', function(done) {
+ zone.makeReq_ = function(method, path, query, body) {
+ assert.strictEqual(
+ body.machineType,
+ 'zones/' + ZONE_NAME + '/machineTypes/' + CONFIG.machineType
+ );
+ done();
+ };
+
+ zone.createVM(NAME, CONFIG, assert.ifError);
+ });
+ });
+
+ describe('config.tags', function() {
+ var CONFIG = {
+ tags: ['a', 'b']
+ };
+
+ it('should accept an array of tags', function(done) {
+ zone.makeReq_ = function(method, path, query, body) {
+ assert.deepEqual(body.tags.items, CONFIG.tags);
+ done();
+ };
+
+ zone.createVM(NAME, CONFIG, assert.ifError);
+ });
+ });
+
+ describe('config.http', function() {
+ var CONFIG = {
+ http: true
+ };
+
+ it('should add a network interface accessConfig', function(done) {
+ zone.makeReq_ = function(method, path, query, body) {
+ assert.deepEqual(body.networkInterfaces[0].accessConfigs[0], {
+ type: 'ONE_TO_ONE_NAT'
+ });
+ done();
+ };
+
+ zone.createVM(NAME, CONFIG, assert.ifError);
+ });
+
+ it('should add an http tag', function(done) {
+ zone.makeReq_ = function(method, path, query, body) {
+ assert(body.tags.items.indexOf('http-server') > -1);
+ done();
+ };
+
+ zone.createVM(NAME, CONFIG, assert.ifError);
+ });
+
+ it('should not overwrite existing tags', function(done) {
+ var config = {
+ http: true,
+ tags: {
+ items: ['a', 'b']
+ }
+ };
+
+ var expectedTags = ['a', 'b', 'http-server'];
+
+ zone.makeReq_ = function(method, path, query, body) {
+ assert.deepEqual(body.tags.items, expectedTags);
+ done();
+ };
+
+ zone.createVM(NAME, config, assert.ifError);
+ });
+
+ it('should delete the https property', function(done) {
+ zone.makeReq_ = function(method, path, query, body) {
+ assert.strictEqual(body.https, undefined);
+ done();
+ };
+
+ zone.createVM(NAME, CONFIG, assert.ifError);
+ });
+ });
+
+ describe('config.https', function() {
+ var CONFIG = {
+ https: true
+ };
+
+ it('should add a network interface accessConfig', function(done) {
+ zone.makeReq_ = function(method, path, query, body) {
+ assert.deepEqual(body.networkInterfaces[0].accessConfigs[0], {
+ type: 'ONE_TO_ONE_NAT'
+ });
+ done();
+ };
+
+ zone.createVM(NAME, CONFIG, assert.ifError);
+ });
+
+ it('should add an https tag', function(done) {
+ zone.makeReq_ = function(method, path, query, body) {
+ assert(body.tags.items.indexOf('https-server') > -1);
+ done();
+ };
+
+ zone.createVM(NAME, CONFIG, assert.ifError);
+ });
+
+ it('should not overwrite existing tags', function(done) {
+ var config = {
+ https: true,
+ tags: {
+ items: ['a', 'b']
+ }
+ };
+
+ var expectedTags = ['a', 'b', 'https-server'];
+
+ zone.makeReq_ = function(method, path, query, body) {
+ assert.deepEqual(body.tags.items, expectedTags);
+ done();
+ };
+
+ zone.createVM(NAME, config, assert.ifError);
+ });
+
+ it('should delete the https property', function(done) {
+ zone.makeReq_ = function(method, path, query, body) {
+ assert.strictEqual(body.https, undefined);
+ done();
+ };
+
+ zone.createVM(NAME, CONFIG, assert.ifError);
+ });
+ });
+
+ describe('config.os', function() {
+ var CONFIG = {
+ os: 'os-name'
+ };
+
+ it('should get the latest image', function(done) {
+ zone.gceImages.getLatest = function(os) {
+ assert.strictEqual(os, CONFIG.os);
+ done();
+ };
+
+ zone.createVM(NAME, CONFIG, assert.ifError);
+ });
+
+ describe('error', function() {
+ var error = new Error('Error.');
+
+ beforeEach(function() {
+ zone.gceImages.getLatest = function(os, callback) {
+ callback(error);
+ };
+ });
+
+ it('should execute callback with error', function(done) {
+ zone.createVM(NAME, CONFIG, function(err) {
+ assert.strictEqual(err, error);
+ done();
+ });
+ });
+ });
+
+ describe('success', function() {
+ var gceImagesResp = {
+ selfLink: 'http://selflink'
+ };
+
+ var expectedConfig = extend({}, EXPECTED_CONFIG, {
+ disks: [
+ {
+ boot: true,
+ initializeParams: {
+ sourceImage: gceImagesResp.selfLink
+ }
+ }
+ ]
+ });
+
+ it('should call createVM with the correct config', function(done) {
+ zone.gceImages.getLatest = function(os, callback) {
+ zone.createVM = function(name, config, callback) {
+ assert.strictEqual(name, NAME);
+ assert.deepEqual(config, expectedConfig);
+ callback();
+ };
+
+ callback(null, gceImagesResp);
+ };
+
+ zone.createVM(NAME, CONFIG, done);
+ });
+ });
+ });
+
+ describe('API request', function() {
+ it('should make the correct API request', function(done) {
+ zone.makeReq_ = function(method, path, query, body) {
+ assert.strictEqual(method, 'POST');
+ assert.strictEqual(path, '/instances');
+ assert.deepEqual(query, null);
+ assert.deepEqual(body, EXPECTED_CONFIG);
+
+ done();
+ };
+
+ zone.createVM(NAME, CONFIG, assert.ifError);
+ });
+
+ describe('error', function() {
+ var error = new Error('Error.');
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ zone.makeReq_ = function(method, path, query, body, callback) {
+ callback(error, apiResponse);
+ };
+ });
+
+ it('should execute callback with error & API response', function(done) {
+ zone.createVM(NAME, CONFIG, function(err, vm, op, apiResp) {
+ assert.strictEqual(err, error);
+ assert.strictEqual(vm, null);
+ assert.strictEqual(op, null);
+ assert.strictEqual(apiResp, apiResponse);
+ done();
+ });
+ });
+ });
+
+ describe('success', function() {
+ var apiResponse = { name: 'operation-name' };
+
+ beforeEach(function() {
+ zone.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponse);
+ };
+ });
+
+ it('should exec callback with Disk, Op & apiResponse', function(done) {
+ var vm = {};
+ var operation = {};
+
+ zone.vm = function(name) {
+ assert.strictEqual(name, NAME);
+ return vm;
+ };
+
+ zone.operation = function(name) {
+ assert.strictEqual(name, apiResponse.name);
+ return operation;
+ };
+
+ zone.createVM(NAME, CONFIG, function(err, vm_, op, apiResp) {
+ assert.ifError(err);
+
+ assert.strictEqual(vm_, vm);
+
+ assert.strictEqual(op, operation);
+ assert.strictEqual(op.metadata, apiResp);
+
+ assert.strictEqual(apiResp, apiResponse);
+ done();
+ });
+ });
+ });
+ });
+ });
+
+ describe('disk', function() {
+ var NAME = 'disk-name';
+
+ it('should return a Disk object', function() {
+ var disk = zone.disk(NAME);
+ assert(disk instanceof FakeDisk);
+ assert.strictEqual(disk.calledWith_[0], zone);
+ assert.strictEqual(disk.calledWith_[1], NAME);
+ });
+ });
+
+ describe('getDisks', function() {
+ it('should accept only a callback', function(done) {
+ zone.makeReq_ = function(method, path, query) {
+ assert.deepEqual(query, {});
+ done();
+ };
+
+ zone.getDisks(assert.ifError);
+ });
+
+ it('should make the correct API request', function(done) {
+ var query = { a: 'b', c: 'd' };
+
+ zone.makeReq_ = function(method, path, query_, body) {
+ assert.strictEqual(method, 'GET');
+ assert.strictEqual(path, '/disks');
+ assert.strictEqual(query_, query);
+ assert.strictEqual(body, null);
+
+ done();
+ };
+
+ zone.getDisks(query, assert.ifError);
+ });
+
+ describe('error', function() {
+ var error = new Error('Error.');
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ zone.makeReq_ = function(method, path, query, body, callback) {
+ callback(error, apiResponse);
+ };
+ });
+
+ it('should execute callback with error & API response', function(done) {
+ zone.getDisks({}, function(err, disks, nextQuery, apiResp) {
+ assert.strictEqual(err, error);
+ assert.strictEqual(disks, null);
+ assert.strictEqual(nextQuery, null);
+ assert.strictEqual(apiResp, apiResponse);
+ done();
+ });
+ });
+ });
+
+ describe('success', function() {
+ var apiResponse = {
+ items: [
+ { name: 'disk-name' }
+ ]
+ };
+
+ beforeEach(function() {
+ zone.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponse);
+ };
+ });
+
+ it('should build a nextQuery if necessary', function(done) {
+ var nextPageToken = 'next-page-token';
+ var apiResponseWithNextPageToken = extend({}, apiResponse, {
+ nextPageToken: nextPageToken
+ });
+ var expectedNextQuery = {
+ pageToken: nextPageToken
+ };
+
+ zone.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponseWithNextPageToken);
+ };
+
+ zone.getDisks({}, function(err, disks, nextQuery) {
+ assert.ifError(err);
+
+ assert.deepEqual(nextQuery, expectedNextQuery);
+
+ done();
+ });
+ });
+
+ it('should execute callback with Disks & API resp', function(done) {
+ var disk = {};
+
+ zone.disk = function(name) {
+ assert.strictEqual(name, apiResponse.items[0].name);
+ return disk;
+ };
+
+ zone.getDisks({}, function(err, disks, nextQuery, apiResp) {
+ assert.ifError(err);
+
+ assert.strictEqual(disks[0], disk);
+ assert.strictEqual(disks[0].metadata, apiResponse.items[0]);
+
+ assert.strictEqual(apiResp, apiResponse);
+
+ done();
+ });
+ });
+ });
+ });
+
+ describe('getMetadata', function() {
+ it('should make the correct API request', function(done) {
+ zone.makeReq_ = function(method, path, query, body) {
+ assert.strictEqual(method, 'GET');
+ assert.strictEqual(path, '');
+ assert.strictEqual(query, null);
+ assert.strictEqual(body, null);
+
+ done();
+ };
+
+ zone.getMetadata(assert.ifError);
+ });
+
+ describe('error', function() {
+ var error = new Error('Error.');
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ zone.makeReq_ = function(method, path, query, body, callback) {
+ callback(error, apiResponse);
+ };
+ });
+
+ it('should execute callback with error and API response', function(done) {
+ zone.getMetadata(function(err, metadata, apiResponse_) {
+ assert.strictEqual(err, error);
+ assert.strictEqual(metadata, null);
+ assert.strictEqual(apiResponse_, apiResponse);
+ done();
+ });
+ });
+ });
+
+ describe('success', function() {
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ zone.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponse);
+ };
+ });
+
+ it('should update the metadata to the API response', function(done) {
+ zone.getMetadata(function(err) {
+ assert.ifError(err);
+ assert.strictEqual(zone.metadata, apiResponse);
+ done();
+ });
+ });
+
+ it('should exec callback with metadata and API response', function(done) {
+ zone.getMetadata(function(err, metadata, apiResponse_) {
+ assert.ifError(err);
+ assert.strictEqual(metadata, apiResponse);
+ assert.strictEqual(apiResponse_, apiResponse);
+ done();
+ });
+ });
+ });
+ });
+
+ describe('getOperations', function() {
+ it('should accept only a callback', function(done) {
+ zone.makeReq_ = function(method, path, query) {
+ assert.deepEqual(query, {});
+ done();
+ };
+
+ zone.getOperations(assert.ifError);
+ });
+
+ it('should make the correct API request', function(done) {
+ var query = { a: 'b', c: 'd' };
+
+ zone.makeReq_ = function(method, path, query_, body) {
+ assert.strictEqual(method, 'GET');
+ assert.strictEqual(path, '/operations');
+ assert.strictEqual(query_, query);
+ assert.strictEqual(body, null);
+
+ done();
+ };
+
+ zone.getOperations(query, assert.ifError);
+ });
+
+ describe('error', function() {
+ var error = new Error('Error.');
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ zone.makeReq_ = function(method, path, query, body, callback) {
+ callback(error, apiResponse);
+ };
+ });
+
+ it('should execute callback with error & API response', function(done) {
+ zone.getOperations({}, function(err, operations, nextQuery, apiResp) {
+ assert.strictEqual(err, error);
+ assert.strictEqual(nextQuery, null);
+ assert.strictEqual(apiResp, apiResponse);
+ done();
+ });
+ });
+ });
+
+ describe('success', function() {
+ var apiResponse = {
+ items: [
+ { name: 'operation-name' }
+ ]
+ };
+
+ beforeEach(function() {
+ zone.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponse);
+ };
+ });
+
+ it('should build a nextQuery if necessary', function(done) {
+ var nextPageToken = 'next-page-token';
+ var apiResponseWithNextPageToken = extend({}, apiResponse, {
+ nextPageToken: nextPageToken
+ });
+ var expectedNextQuery = {
+ pageToken: nextPageToken
+ };
+
+ zone.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponseWithNextPageToken);
+ };
+
+ zone.getOperations({}, function(err, operations, nextQuery) {
+ assert.ifError(err);
+
+ assert.deepEqual(nextQuery, expectedNextQuery);
+
+ done();
+ });
+ });
+
+ it('should execute callback with Operations & API resp', function(done) {
+ var operation = {};
+
+ zone.operation = function(name) {
+ assert.strictEqual(name, apiResponse.items[0].name);
+ return operation;
+ };
+
+ zone.getOperations({}, function(err, operations, nextQuery, apiResp) {
+ assert.ifError(err);
+
+ assert.strictEqual(operations[0], operation);
+ assert.strictEqual(operations[0].metadata, apiResponse.items[0]);
+
+ assert.strictEqual(apiResp, apiResponse);
+
+ done();
+ });
+ });
+ });
+ });
+
+ describe('getVMs', function() {
+ it('should accept only a callback', function(done) {
+ zone.makeReq_ = function(method, path, query) {
+ assert.deepEqual(query, {});
+ done();
+ };
+
+ zone.getVMs(assert.ifError);
+ });
+
+ it('should make the correct API request', function(done) {
+ var query = { a: 'b', c: 'd' };
+
+ zone.makeReq_ = function(method, path, query_, body) {
+ assert.strictEqual(method, 'GET');
+ assert.strictEqual(path, '/instances');
+ assert.strictEqual(query_, query);
+ assert.strictEqual(body, null);
+
+ done();
+ };
+
+ zone.getVMs(query, assert.ifError);
+ });
+
+ describe('error', function() {
+ var error = new Error('Error.');
+ var apiResponse = { a: 'b', c: 'd' };
+
+ beforeEach(function() {
+ zone.makeReq_ = function(method, path, query, body, callback) {
+ callback(error, apiResponse);
+ };
+ });
+
+ it('should execute callback with error & API response', function(done) {
+ zone.getVMs({}, function(err, vms, nextQuery, apiResponse_) {
+ assert.strictEqual(err, error);
+ assert.strictEqual(nextQuery, null);
+ assert.strictEqual(apiResponse_, apiResponse);
+ done();
+ });
+ });
+ });
+
+ describe('success', function() {
+ var apiResponse = {
+ items: [
+ { name: 'vm-name' }
+ ]
+ };
+
+ beforeEach(function() {
+ zone.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponse);
+ };
+ });
+
+ it('should build a nextQuery if necessary', function(done) {
+ var nextPageToken = 'next-page-token';
+ var apiResponseWithNextPageToken = extend({}, apiResponse, {
+ nextPageToken: nextPageToken
+ });
+ var expectedNextQuery = {
+ pageToken: nextPageToken
+ };
+
+ zone.makeReq_ = function(method, path, query, body, callback) {
+ callback(null, apiResponseWithNextPageToken);
+ };
+
+ zone.getVMs({}, function(err, vms, nextQuery) {
+ assert.ifError(err);
+
+ assert.deepEqual(nextQuery, expectedNextQuery);
+
+ done();
+ });
+ });
+
+ it('should execute callback with VMs & API response', function(done) {
+ var vm = {};
+
+ zone.vm = function(name) {
+ assert.strictEqual(name, apiResponse.items[0].name);
+ return vm;
+ };
+
+ zone.getVMs({}, function(err, vms, nextQuery, apiResponse_) {
+ assert.ifError(err);
+
+ assert.strictEqual(vms[0], vm);
+ assert.strictEqual(vms[0].metadata, apiResponse.items[0]);
+
+ assert.strictEqual(apiResponse_, apiResponse);
+
+ done();
+ });
+ });
+ });
+ });
+
+ describe('operation', function() {
+ var NAME = 'operation-name';
+
+ it('should return an Operation object', function() {
+ var operation = zone.operation(NAME);
+ assert(operation instanceof FakeOperation);
+ assert.strictEqual(operation.calledWith_[0], zone);
+ assert.strictEqual(operation.calledWith_[1], NAME);
+ });
+ });
+
+ describe('vm', function() {
+ var NAME = 'vm-name';
+
+ it('should return a VM object', function() {
+ var vm = zone.vm(NAME);
+ assert(vm instanceof FakeVM);
+ assert.strictEqual(vm.calledWith_[0], zone);
+ assert.strictEqual(vm.calledWith_[1], NAME);
+ });
+ });
+
+ describe('makeReq_', function() {
+ it('should make the correct request to Compute', function(done) {
+ var expectedPathPrefix = '/zones/' + zone.name;
+
+ var method = 'POST';
+ var path = '/test';
+ var query = {
+ a: 'b',
+ c: 'd'
+ };
+ var body = {
+ a: 'b',
+ c: 'd'
+ };
+
+ zone.compute.makeReq_ = function(method_, path_, query_, body_, cb) {
+ assert.strictEqual(method_, method);
+ assert.strictEqual(path_, expectedPathPrefix + path);
+ assert.strictEqual(query_, query);
+ assert.strictEqual(body_, body);
+ cb();
+ };
+
+ zone.makeReq_(method, path, query, body, done);
+ });
+ });
+});