@@ -41,9 +41,40 @@ var Disk = require('./disk.js');
4141 * @param {string } message - Custom error message.
4242 * @return {Error }
4343 */
44- var DetachDiskError = createErrorClass ( 'DetachDiskError' , function ( message ) {
45- this . message = message ;
46- } ) ;
44+ var DetachDiskError = createErrorClass ( 'DetachDiskError' ) ;
45+
46+ /**
47+ * Custom error type for when `waitFor()` does not return a status in a timely
48+ * fashion.
49+ *
50+ * @private
51+ *
52+ * @param {string } message - Custom error message.
53+ * @return {Error }
54+ */
55+ var WaitForTimeoutError = createErrorClass ( 'WaitForTimeoutError' ) ;
56+
57+ /**
58+ * The statuses that a VM can be in.
59+ *
60+ * @private
61+ */
62+ var VALID_STATUSES = [
63+ 'PROVISIONING' ,
64+ 'STAGING' ,
65+ 'RUNNING' ,
66+ 'STOPPING' ,
67+ 'SUSPENDING' ,
68+ 'SUSPENDED' ,
69+ 'TERMINATED'
70+ ] ;
71+
72+ /**
73+ * Interval for polling during waitFor.
74+ *
75+ * @private
76+ */
77+ var WAIT_FOR_POLLING_INTERVAL_MS = 2000 ;
4778
4879/*! Developer Documentation
4980 *
@@ -69,6 +100,9 @@ function VM(zone, name) {
69100 this . name = name . replace ( / .* \/ ( [ ^ / ] + ) $ / , '$1' ) ; // Just the instance name.
70101 this . zone = zone ;
71102
103+ this . hasActiveWaiters = false ;
104+ this . waiters = [ ] ;
105+
72106 this . url = format ( '{base}/{project}/zones/{zone}/instances/{name}' , {
73107 base : 'https://www.googleapis.com/compute/v1/projects' ,
74108 project : zone . compute . projectId ,
@@ -779,6 +813,145 @@ VM.prototype.stop = function(callback) {
779813 } , callback || common . util . noop ) ;
780814} ;
781815
816+ /**
817+ * This function will callback when the VM is in the specified state.
818+ *
819+ * Will time out after the specified time (default: 300 seconds).
820+ *
821+ * @param {string } status - The status to wait for. This can be:
822+ * - "PROVISIONING"
823+ * - "STAGING"
824+ * - "RUNNING"
825+ * - "STOPPING"
826+ * - "SUSPENDING"
827+ * - "SUSPENDED"
828+ * - "TERMINATED"
829+ * @param {object= } options - Configuration object.
830+ * @param {number } options.timeout - The number of seconds to wait until timing
831+ * out, between `0` and `600`. Default: `300`
832+ * @param {function } callback - The callback function.
833+ * @param {?error } callback.err - An error returned while waiting for the
834+ * status.
835+ * @param {object } callback.metadata - The instance's metadata.
836+ *
837+ * @example
838+ * vm.waitFor('RUNNING', function(err, metadata) {
839+ * if (!err) {
840+ * // The VM is running.
841+ * }
842+ * });
843+ *
844+ * //-
845+ * // By default, `waitFor` will timeout after 300 seconds while waiting for the
846+ * // desired state to occur. This can be changed to any number between 0 and
847+ * // 600. If the timeout is set to 0, it will poll once for status and then
848+ * // timeout if the desired state is not reached.
849+ * //-
850+ * var options = {
851+ * timeout: 600
852+ * };
853+ *
854+ * vm.waitFor('TERMINATED', options, function(err, metadata) {
855+ * if (!err) {
856+ * // The VM is terminated.
857+ * }
858+ * });
859+ *
860+ * //-
861+ * // If the callback is omitted, we'll return a Promise.
862+ * //-
863+ * vm.waitFor('RUNNING', options).then(function(data) {
864+ * var metadata = data[0];
865+ * });
866+ */
867+ VM . prototype . waitFor = function ( status , options , callback ) {
868+ if ( is . fn ( options ) ) {
869+ callback = options ;
870+ options = { } ;
871+ }
872+
873+ options = options || { } ;
874+
875+ status = status . toUpperCase ( ) ;
876+
877+ // The timeout should default to five minutes, be less than or equal to 10
878+ // minutes, and be greater than or equal to 0 seconds.
879+ var timeout = 300 ;
880+
881+ if ( is . number ( options . timeout ) ) {
882+ timeout = Math . min ( Math . max ( options . timeout , 0 ) , 600 ) ;
883+ }
884+
885+ if ( VALID_STATUSES . indexOf ( status ) === - 1 ) {
886+ throw new Error ( 'Status passed to waitFor is invalid.' ) ;
887+ }
888+
889+ this . waiters . push ( {
890+ status : status ,
891+ timeout : timeout ,
892+ startTime : new Date ( ) / 1000 ,
893+ callback : callback
894+ } ) ;
895+
896+ if ( ! this . hasActiveWaiters ) {
897+ this . hasActiveWaiters = true ;
898+ this . startPolling_ ( ) ;
899+ }
900+ } ;
901+
902+ /**
903+ * Poll `getMetadata` to check the VM's status. This runs a loop to ping
904+ * the API on an interval.
905+ *
906+ * Note: This method is automatically called when a `waitFor()` call
907+ * is made.
908+ *
909+ * @private
910+ */
911+ VM . prototype . startPolling_ = function ( ) {
912+ var self = this ;
913+
914+ if ( ! this . hasActiveWaiters ) {
915+ return ;
916+ }
917+
918+ this . getMetadata ( function ( err , metadata ) {
919+ var now = new Date ( ) / 1000 ;
920+
921+ var waitersToRemove = self . waiters . filter ( function ( waiter ) {
922+ if ( err ) {
923+ waiter . callback ( err ) ;
924+ return true ;
925+ }
926+
927+ if ( metadata . status === waiter . status ) {
928+ waiter . callback ( null , metadata ) ;
929+ return true ;
930+ }
931+
932+ if ( now - waiter . startTime >= waiter . timeout ) {
933+ var waitForTimeoutError = new WaitForTimeoutError ( [
934+ 'waitFor timed out waiting for VM ' + self . name ,
935+ 'to be in status: ' + waiter . status
936+ ] . join ( ' ' ) ) ;
937+ waiter . callback ( waitForTimeoutError ) ;
938+ return true ;
939+ }
940+ } ) ;
941+
942+ waitersToRemove . forEach ( function ( waiter ) {
943+ self . waiters . splice ( self . waiters . indexOf ( waiter ) , 1 ) ;
944+ } ) ;
945+
946+ self . hasActiveWaiters = self . waiters . length > 0 ;
947+
948+ if ( self . hasActiveWaiters ) {
949+ setTimeout ( self . startPolling_ . bind ( self ) , WAIT_FOR_POLLING_INTERVAL_MS ) ;
950+ }
951+ } ) ;
952+ } ;
953+
954+
782955/**
783956 * Make a new request object from the provided arguments and wrap the callback
784957 * to intercept non-successful responses.
0 commit comments