gcloud 0.0.2 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (118) hide show
  1. data.tar.gz.sig +2 -3
  2. data/CHANGELOG +4 -0
  3. data/LICENSE +674 -0
  4. data/Manifest +111 -0
  5. data/README.md +4 -3
  6. data/bin/gcutil +53 -0
  7. data/gcloud.gemspec +4 -3
  8. data/packages/gcutil-1.7.1/CHANGELOG +197 -0
  9. data/packages/gcutil-1.7.1/LICENSE +202 -0
  10. data/packages/gcutil-1.7.1/VERSION +1 -0
  11. data/packages/gcutil-1.7.1/gcutil +53 -0
  12. data/packages/gcutil-1.7.1/lib/google_api_python_client/LICENSE +23 -0
  13. data/packages/gcutil-1.7.1/lib/google_api_python_client/apiclient/__init__.py +1 -0
  14. data/packages/gcutil-1.7.1/lib/google_api_python_client/apiclient/discovery.py +743 -0
  15. data/packages/gcutil-1.7.1/lib/google_api_python_client/apiclient/errors.py +123 -0
  16. data/packages/gcutil-1.7.1/lib/google_api_python_client/apiclient/ext/__init__.py +0 -0
  17. data/packages/gcutil-1.7.1/lib/google_api_python_client/apiclient/http.py +1443 -0
  18. data/packages/gcutil-1.7.1/lib/google_api_python_client/apiclient/mimeparse.py +172 -0
  19. data/packages/gcutil-1.7.1/lib/google_api_python_client/apiclient/model.py +385 -0
  20. data/packages/gcutil-1.7.1/lib/google_api_python_client/apiclient/schema.py +303 -0
  21. data/packages/gcutil-1.7.1/lib/google_api_python_client/oauth2client/__init__.py +1 -0
  22. data/packages/gcutil-1.7.1/lib/google_api_python_client/oauth2client/anyjson.py +32 -0
  23. data/packages/gcutil-1.7.1/lib/google_api_python_client/oauth2client/appengine.py +528 -0
  24. data/packages/gcutil-1.7.1/lib/google_api_python_client/oauth2client/client.py +1139 -0
  25. data/packages/gcutil-1.7.1/lib/google_api_python_client/oauth2client/clientsecrets.py +105 -0
  26. data/packages/gcutil-1.7.1/lib/google_api_python_client/oauth2client/crypt.py +244 -0
  27. data/packages/gcutil-1.7.1/lib/google_api_python_client/oauth2client/django_orm.py +124 -0
  28. data/packages/gcutil-1.7.1/lib/google_api_python_client/oauth2client/file.py +107 -0
  29. data/packages/gcutil-1.7.1/lib/google_api_python_client/oauth2client/locked_file.py +343 -0
  30. data/packages/gcutil-1.7.1/lib/google_api_python_client/oauth2client/multistore_file.py +379 -0
  31. data/packages/gcutil-1.7.1/lib/google_api_python_client/oauth2client/tools.py +174 -0
  32. data/packages/gcutil-1.7.1/lib/google_api_python_client/uritemplate/__init__.py +147 -0
  33. data/packages/gcutil-1.7.1/lib/google_apputils/LICENSE +202 -0
  34. data/packages/gcutil-1.7.1/lib/google_apputils/google/__init__.py +3 -0
  35. data/packages/gcutil-1.7.1/lib/google_apputils/google/apputils/__init__.py +3 -0
  36. data/packages/gcutil-1.7.1/lib/google_apputils/google/apputils/app.py +356 -0
  37. data/packages/gcutil-1.7.1/lib/google_apputils/google/apputils/appcommands.py +783 -0
  38. data/packages/gcutil-1.7.1/lib/google_apputils/google/apputils/basetest.py +1260 -0
  39. data/packages/gcutil-1.7.1/lib/google_apputils/google/apputils/datelib.py +421 -0
  40. data/packages/gcutil-1.7.1/lib/google_apputils/google/apputils/debug.py +60 -0
  41. data/packages/gcutil-1.7.1/lib/google_apputils/google/apputils/file_util.py +181 -0
  42. data/packages/gcutil-1.7.1/lib/google_apputils/google/apputils/resources.py +67 -0
  43. data/packages/gcutil-1.7.1/lib/google_apputils/google/apputils/run_script_module.py +217 -0
  44. data/packages/gcutil-1.7.1/lib/google_apputils/google/apputils/setup_command.py +159 -0
  45. data/packages/gcutil-1.7.1/lib/google_apputils/google/apputils/shellutil.py +49 -0
  46. data/packages/gcutil-1.7.1/lib/google_apputils/google/apputils/stopwatch.py +204 -0
  47. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/__init__.py +0 -0
  48. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/auth_helper.py +140 -0
  49. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/auth_helper_test.py +149 -0
  50. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/auto_auth.py +130 -0
  51. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/auto_auth_test.py +75 -0
  52. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/basic_cmds.py +128 -0
  53. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/basic_cmds_test.py +111 -0
  54. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/command_base.py +1808 -0
  55. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/command_base_test.py +1651 -0
  56. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/compute/v1beta13.json +2851 -0
  57. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/compute/v1beta14.json +3361 -0
  58. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/disk_cmds.py +342 -0
  59. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/disk_cmds_test.py +474 -0
  60. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/firewall_cmds.py +344 -0
  61. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/firewall_cmds_test.py +231 -0
  62. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/flags_cache.py +274 -0
  63. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/gcutil +89 -0
  64. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/gcutil_logging.py +69 -0
  65. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/image_cmds.py +262 -0
  66. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/image_cmds_test.py +172 -0
  67. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/instance_cmds.py +1506 -0
  68. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/instance_cmds_test.py +1904 -0
  69. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/kernel_cmds.py +91 -0
  70. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/kernel_cmds_test.py +56 -0
  71. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/machine_type_cmds.py +106 -0
  72. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/machine_type_cmds_test.py +59 -0
  73. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/metadata.py +96 -0
  74. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/metadata_lib.py +357 -0
  75. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/metadata_test.py +84 -0
  76. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/mock_api.py +420 -0
  77. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/mock_metadata.py +58 -0
  78. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/move_cmds.py +824 -0
  79. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/move_cmds_test.py +307 -0
  80. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/network_cmds.py +178 -0
  81. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/network_cmds_test.py +133 -0
  82. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/operation_cmds.py +181 -0
  83. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/operation_cmds_test.py +196 -0
  84. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/path_initializer.py +38 -0
  85. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/project_cmds.py +173 -0
  86. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/project_cmds_test.py +111 -0
  87. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/scopes.py +61 -0
  88. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/scopes_test.py +50 -0
  89. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/snapshot_cmds.py +276 -0
  90. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/snapshot_cmds_test.py +260 -0
  91. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/ssh_keys.py +266 -0
  92. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/ssh_keys_test.py +128 -0
  93. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/table_formatter.py +563 -0
  94. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/thread_pool.py +188 -0
  95. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/thread_pool_test.py +88 -0
  96. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/utils.py +208 -0
  97. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/utils_test.py +193 -0
  98. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/version.py +17 -0
  99. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/version_checker.py +246 -0
  100. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/version_checker_test.py +271 -0
  101. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/zone_cmds.py +151 -0
  102. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/zone_cmds_test.py +60 -0
  103. data/packages/gcutil-1.7.1/lib/httplib2/LICENSE +21 -0
  104. data/packages/gcutil-1.7.1/lib/httplib2/httplib2/__init__.py +1630 -0
  105. data/packages/gcutil-1.7.1/lib/httplib2/httplib2/cacerts.txt +714 -0
  106. data/packages/gcutil-1.7.1/lib/httplib2/httplib2/iri2uri.py +110 -0
  107. data/packages/gcutil-1.7.1/lib/httplib2/httplib2/socks.py +438 -0
  108. data/packages/gcutil-1.7.1/lib/iso8601/LICENSE +20 -0
  109. data/packages/gcutil-1.7.1/lib/iso8601/iso8601/__init__.py +1 -0
  110. data/packages/gcutil-1.7.1/lib/iso8601/iso8601/iso8601.py +102 -0
  111. data/packages/gcutil-1.7.1/lib/iso8601/iso8601/test_iso8601.py +111 -0
  112. data/packages/gcutil-1.7.1/lib/python_gflags/AUTHORS +2 -0
  113. data/packages/gcutil-1.7.1/lib/python_gflags/LICENSE +28 -0
  114. data/packages/gcutil-1.7.1/lib/python_gflags/gflags.py +2862 -0
  115. data/packages/gcutil-1.7.1/lib/python_gflags/gflags2man.py +544 -0
  116. data/packages/gcutil-1.7.1/lib/python_gflags/gflags_validators.py +187 -0
  117. metadata +118 -5
  118. metadata.gz.sig +0 -0
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/python
2
+ #
3
+ # Copyright 2012 Google Inc. All Rights Reserved.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ """Unit tests for the compute commands."""
18
+
19
+
20
+
21
+ import path_initializer
22
+ path_initializer.InitializeSysPath()
23
+
24
+ import copy
25
+ import re
26
+ import tempfile
27
+
28
+
29
+ import gflags as flags
30
+ import unittest
31
+
32
+ from gcutil import auth_helper
33
+ from gcutil import basic_cmds
34
+ from gcutil import command_base
35
+ from gcutil import flags_cache
36
+ from gcutil import mock_api
37
+
38
+ FLAGS = flags.FLAGS
39
+
40
+
41
+ class ComputeCmdsTest(unittest.TestCase):
42
+ def testAuthDoesNotBuildApi(self):
43
+ class MockFlagsCache(object):
44
+ """Mock FlagsCache for testing."""
45
+
46
+ def SynchronizeFlags(self):
47
+ """Mock SynchronizeFlags method that does nothing."""
48
+ pass
49
+
50
+ class MockCredential(object):
51
+ """Mock OAuth2 Credential."""
52
+
53
+ def authorize(self, http):
54
+ """Authorize an http2.Http instance with this credential.
55
+
56
+ Args:
57
+ http: httplib2.http to append authorization to.
58
+
59
+ Returns:
60
+ An httplib2.http compatible object.
61
+ """
62
+ return http
63
+
64
+ def MockGetCredentialFromStore(scopes,
65
+ ask_user,
66
+ force_reauth):
67
+ """Returns a mock cred.
68
+
69
+ Args:
70
+ scopes: Scopes for which auth is being requested.
71
+ ask_user: Should the user be asked to auth?
72
+ force_reauth: Force user to reauth.
73
+
74
+ Returns:
75
+ A credentials object.
76
+ """
77
+ force_reauth = force_reauth # silence lint
78
+ ask_user = ask_user
79
+ scopes = scopes
80
+ return MockCredential()
81
+
82
+ flag_values = copy.deepcopy(FLAGS)
83
+ flags_cache.FlagsCache = MockFlagsCache
84
+ auth_helper.GetCredentialFromStore = MockGetCredentialFromStore
85
+
86
+ command = basic_cmds.AuthCommand('auth', flag_values)
87
+
88
+ flag_values.fetch_discovery = False
89
+ flag_values.api_host = None
90
+ flag_values.service_version = None
91
+ flag_values.project = None
92
+ flag_values.force_reauth = True
93
+ flag_values.confirm_email = False
94
+
95
+ command.SetFlags(flag_values)
96
+ result = command.RunWithFlagsAndPositionalArgs(flag_values,
97
+ ['path/to/gcutil'])
98
+ self.assertEqual(result, (None, []))
99
+
100
+ def testGetVersionGeneratesCorrectResponse(self):
101
+ flag_values = copy.deepcopy(FLAGS)
102
+ command = basic_cmds.GetVersion('version', flag_values)
103
+ result = command.Run([])
104
+
105
+ self.assertEqual(result, 0)
106
+ self.assertEqual(
107
+ basic_cmds.version.__version__, '1.7.1')
108
+
109
+
110
+ if __name__ == '__main__':
111
+ unittest.main()
@@ -0,0 +1,1808 @@
1
+ # Copyright 2012 Google Inc. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Base command types for interacting with Google Compute Engine."""
16
+
17
+
18
+
19
+ import datetime
20
+ import httplib
21
+ import inspect
22
+ import json
23
+ import os
24
+ import re
25
+ import sys
26
+ import time
27
+ import traceback
28
+
29
+
30
+ from apiclient import discovery
31
+ from apiclient import errors
32
+ from apiclient import model
33
+ import httplib2
34
+ import iso8601
35
+ import oauth2client.client as oauth2_client
36
+
37
+
38
+ from google.apputils import app
39
+ from google.apputils import appcommands
40
+ import gflags as flags
41
+
42
+ from gcutil import auth_helper
43
+ from gcutil import flags_cache
44
+ from gcutil import gcutil_logging
45
+ from gcutil import metadata_lib
46
+ from gcutil import scopes
47
+ from gcutil import thread_pool
48
+ from gcutil import utils
49
+ from gcutil import version
50
+ from gcutil import table_formatter
51
+
52
+ FLAGS = flags.FLAGS
53
+ LOGGER = gcutil_logging.LOGGER
54
+ CLIENT_ID = 'google-api-client-python-compute-cmdline/1.0'
55
+
56
+ CURRENT_VERSION = version.__default_api_version__
57
+ SUPPORTED_VERSIONS = version.__supported_api_versions__
58
+
59
+ GLOBAL_ZONE_NAME = 'global'
60
+
61
+ # The ordering to impose on machine types when prompting the user for
62
+ # a machine type choice.
63
+ MACHINE_TYPE_ORDERING = ['standard', 'highcpu', 'highmem']
64
+
65
+
66
+ flags.DEFINE_enum(
67
+ 'service_version',
68
+ CURRENT_VERSION,
69
+ SUPPORTED_VERSIONS,
70
+ 'Google computation service version.')
71
+ flags.DEFINE_string(
72
+ 'api_host',
73
+ 'https://www.googleapis.com/',
74
+ 'API host name')
75
+ flags.DEFINE_string(
76
+ 'project',
77
+ None,
78
+ 'The name of the Google Compute Engine project.')
79
+ flags.DEFINE_string(
80
+ 'project_id',
81
+ None,
82
+ 'The name of the Google Compute Engine project. '
83
+ 'Deprecated, use --project instead.')
84
+ flags.DEFINE_bool(
85
+ 'print_json',
86
+ False,
87
+ 'Output JSON instead of tabular format. Deprecated, use --format=json.')
88
+ flags.DEFINE_enum(
89
+ 'format', 'table',
90
+ ('table', 'sparse', 'json', 'csv', 'names'),
91
+ 'Format for command output. Options include:'
92
+ '\n table: formatted table output'
93
+ '\n sparse: simpler table output'
94
+ '\n json: raw json output (formerly --print_json)'
95
+ '\n csv: csv format with header'
96
+ '\n names: list of resource names only, no header')
97
+ flags.DEFINE_enum(
98
+ 'long_values_display_format',
99
+ 'elided',
100
+ ['elided', 'full'],
101
+ 'The display preference for long table values.')
102
+ flags.DEFINE_bool(
103
+ 'fetch_discovery',
104
+ False,
105
+ 'If true, grab the API description from the discovery API.')
106
+ flags.DEFINE_bool(
107
+ 'synchronous_mode',
108
+ True,
109
+ 'If false, return immediately after posting a request.')
110
+ flags.DEFINE_integer(
111
+ 'sleep_between_polls',
112
+ 3,
113
+ 'The time to sleep between polls to the server in seconds.',
114
+ 1, 600)
115
+ flags.DEFINE_integer(
116
+ 'max_wait_time',
117
+ 240,
118
+ 'The maximum time to wait for an asynchronous operation to complete in '
119
+ 'seconds.',
120
+ 30, 1200)
121
+ flags.DEFINE_string(
122
+ 'trace_token',
123
+ None,
124
+ 'Trace the API requests using a trace token provided by Google.')
125
+ flags.DEFINE_integer(
126
+ 'concurrent_operations',
127
+ 10,
128
+ 'The maximum number of concurrent operations to have in progress at once. '
129
+ 'Increasing this number will probably result in hitting rate limits.',
130
+ 1, 20)
131
+
132
+
133
+ class Error(Exception):
134
+ """The base class for this tool's error reporting infrastructure."""
135
+
136
+
137
+ class CommandError(Error):
138
+ """Raised when a command hits a general error."""
139
+
140
+
141
+ # A wrapper around an Api that adds a trace keyword to the Api.
142
+ class TracedApi(object):
143
+ """Wrap an Api to add a trace keyword argument."""
144
+
145
+ def __init__(self, obj, trace_token):
146
+ def Wrap(func):
147
+ def _Wrapped(*args, **kwargs):
148
+ # Add a trace= URL parameter to the method call.
149
+ if trace_token:
150
+ kwargs['trace'] = trace_token
151
+ return func(*args, **kwargs)
152
+ return _Wrapped
153
+
154
+ # Find all public methods and interpose them.
155
+ for method in inspect.getmembers(obj, (inspect.ismethod)):
156
+ if not method[0].startswith('__'):
157
+ setattr(self, method[0], Wrap(method[1]))
158
+
159
+
160
+ class TracedComputeApi(object):
161
+ """Wrap a ComputeApi object to return TracedApis."""
162
+
163
+ def __init__(self, obj, trace_token):
164
+ def Wrap(func):
165
+ def _Wrapped(*args, **kwargs):
166
+ ret = func(*args, **kwargs)
167
+ if ret:
168
+ ret = TracedApi(ret, trace_token)
169
+ return ret
170
+ return _Wrapped
171
+
172
+ # Find all our public methods and interpose them.
173
+ for method in inspect.getmembers(obj, (inspect.ismethod)):
174
+ if not method[0].startswith('__'):
175
+ setattr(self, method[0], Wrap(method[1]))
176
+
177
+
178
+ class ApiThreadPoolOperation(thread_pool.Operation):
179
+ """A Thread pool operation that will execute an API request.
180
+
181
+ This will wait for the operation to complete, if appropriate. The
182
+ result from the object will be the last operation object returned.
183
+ """
184
+
185
+ def __init__(self, request, command, wait_for_operation,
186
+ collection_name=None):
187
+ """Initializer."""
188
+ super(ApiThreadPoolOperation, self).__init__()
189
+ self._request = request
190
+ self._command = command
191
+ self._wait_for_operation = wait_for_operation
192
+ self._collection_name = collection_name
193
+
194
+ def Run(self):
195
+ """Execute the request on a separate thread."""
196
+ # Note that the httplib2.Http command isn't thread safe. As such,
197
+ # we need to create a new Http object here.
198
+ http = self._command.CreateHttp()
199
+ result = self._request.execute(http=http)
200
+ if self._wait_for_operation:
201
+ result = self._command.WaitForOperation(
202
+ self._command.GetFlags(), time, result, http=http,
203
+ collection_name=self._collection_name)
204
+ return result
205
+
206
+
207
+ class GoogleComputeCommand(appcommands.Cmd):
208
+ """Base class for commands that interact with the Google Compute Engine API.
209
+
210
+ Overriding classes must override the SetApi and Handle methods.
211
+
212
+ Attributes:
213
+ GOOGLE_PROJECT_PATH: The common 'google' project used for storage of shared
214
+ images and kernels.
215
+ operation_detail_fields: A set of tuples of (json field name, human
216
+ readable name) used to generate a pretty-printed detailed description
217
+ of an operation resource.
218
+ supported_versions: The list of API versions supported by this tool.
219
+ safety_prompt: A boolean indicating whether the command requires user
220
+ confirmation prior to executing.
221
+ """
222
+
223
+ GOOGLE_PROJECT_PATH = 'projects/google'
224
+
225
+ operation_default_sort_field = 'insert-time'
226
+ operation_summary_fields = (('name', 'name'),
227
+ ('zone', 'zone'),
228
+ ('status', 'status'),
229
+ ('status-message', 'statusMessage'),
230
+ ('target', 'targetLink'),
231
+ ('insert-time', 'insertTime'),
232
+ ('operation-type', 'operationType'),
233
+ ('error', 'error.errors.code'),
234
+ ('warning', 'warnings.code'))
235
+ operation_detail_fields = (('name', 'name'),
236
+ ('zone', 'zone'),
237
+ ('creation-time', 'creationTimestamp'),
238
+ ('status', 'status'),
239
+ ('progress', 'progress'),
240
+ ('status-message', 'statusMessage'),
241
+ ('target', 'targetLink'),
242
+ ('target-id', 'targetId'),
243
+ ('client-operation-id', 'clientOperationId'),
244
+ ('insert-time', 'insertTime'),
245
+ ('user', 'user'),
246
+ ('start-time', 'startTime'),
247
+ ('end-time', 'endTime'),
248
+ ('operation-type', 'operationType'),
249
+ ('error-code', 'httpErrorStatusCode'),
250
+ ('error-message', 'httpErrorMessage'),
251
+ ('warning', 'warnings.code'),
252
+ ('warning-message', 'warnings.message'))
253
+
254
+ # If this is set to True then the arguments and flags for this
255
+ # command are sorted such that everything that looks like a flag is
256
+ # pulled out of the arguments. If a command needs unparsed flags
257
+ # after positional arguments (like ssh) then set this to False.
258
+ sort_args_and_flags = True
259
+
260
+ def __init__(self, name, flag_values):
261
+ """Initializes a new instance of a GoogleComputeCommand.
262
+
263
+ Args:
264
+ name: The name of the command.
265
+ flag_values: The values of command line flags to be used by the command.
266
+ """
267
+ super(GoogleComputeCommand, self).__init__(name, flag_values)
268
+ self._credential = None
269
+ self.supported_versions = SUPPORTED_VERSIONS
270
+
271
+ if hasattr(self, 'safety_prompt'):
272
+ flags.DEFINE_bool('force',
273
+ False,
274
+ 'Override the "%s" prompt' % self.safety_prompt,
275
+ flag_values=flag_values,
276
+ short_name='f')
277
+
278
+ def _ReadInSelectedItem(self, menu, menu_name):
279
+ while True:
280
+ userinput = raw_input('>>> ').strip()
281
+ try:
282
+ selection = int(userinput)
283
+ if selection in menu:
284
+ return selection
285
+ except ValueError:
286
+ pass
287
+ print 'Invalid selection, please choose one of the listed ' + menu_name
288
+
289
+ def _PromptForEntry(self, collection_api, collection_name, project=None,
290
+ auto_select=True, extract_resource_prompt=None,
291
+ additional_key_func=None):
292
+ """Prompt the user to select an entry from an API collection.
293
+
294
+ Args:
295
+ collection_api: The API collection wrapper.
296
+ collection_name: The name of the collection used in building the prompts.
297
+ project: A project whose collection to use. Defaults to self._project.
298
+ auto_select: If True and the collection has a single element then that
299
+ element is chosen without prompting the user.
300
+ extract_resource_prompt: A function that takes a resource JSON and returns
301
+ the resource prompt. If not provided, the resource's 'name' field is
302
+ going to be used as the default prompt text.
303
+ additional_key_func: Lambda resource_name -> int. If supplied, this
304
+ function will be used as the first sort key of the name.
305
+
306
+ Returns:
307
+ A collection entry as selected by the user or None if the collection is
308
+ empty;
309
+ """
310
+ choices = utils.All(collection_api.list, project or self._project)['items']
311
+ return self._PromptForChoice(
312
+ choices, collection_name, auto_select, extract_resource_prompt,
313
+ additional_key_func)
314
+
315
+ def _PromptForChoice(self, choices, collection_name, auto_select=True,
316
+ extract_resource_prompt=None, additional_key_func=None):
317
+ """Prompts user to select one of the resources from the choices list.
318
+
319
+ The function will create list of prompts from the list of choices. If caller
320
+ passed extract_resource_prompt function, the extract_resource_prompt will be
321
+ called on each resource to generate appropriate prompt text.
322
+
323
+ Prompt strings are sorted alphabetically and offered to the user to select
324
+ the desired option. The selected resource is then returned to the caller.
325
+
326
+ If the list of choices is empty, None is returned.
327
+ If there is only one available choice and auto_select is True, user is not
328
+ prompted but rather, the only available option is returned.
329
+
330
+ Args:
331
+ choices: List of Google Compute Engine resources from which user should
332
+ choose.
333
+ collection_name: Name of the collection to present to the user.
334
+ auto_select: Boolean. If set to True and only one resource is available in
335
+ the list of choices, user will not be prompted but rather, the only
336
+ available option will be chosen.
337
+ extract_resource_prompt: Lambda resource -> string. If supplied, this
338
+ function will be called on each resource to generate the prompt string
339
+ for the resource.
340
+ additional_key_func: Lambda resource_name -> int. If supplied, this
341
+ function will be used as the first sort key of the name.
342
+
343
+ Returns:
344
+ The resource user selected. Returns the actual resource as the JSON object
345
+ model represented as Python dictionary.
346
+ """
347
+ if extract_resource_prompt is None:
348
+
349
+ def ExtractResourcePrompt(resource):
350
+ return resource['name'].split('/')[-1]
351
+
352
+ extract_resource_prompt = ExtractResourcePrompt
353
+
354
+ if not choices:
355
+ return None
356
+
357
+ if auto_select and len(choices) == 1:
358
+ print 'Selecting the only available %s: %s' % (
359
+ collection_name, choices[0]['name'])
360
+ if 'deprecated' in choices[0]:
361
+ LOGGER.warn('Warning: %s is deprecated!', choices[0]['name'])
362
+ return choices[0]
363
+
364
+ deprecated_choices = [(extract_resource_prompt(ch) + ' (DEPRECATED)', ch)
365
+ for ch in choices if 'deprecated' in ch
366
+ and ch['deprecated']['state'] == 'DEPRECATED']
367
+ deprecated_choices.sort(key=lambda pair: pair[0])
368
+ choices = [(extract_resource_prompt(ch), ch) for ch in choices
369
+ if not 'deprecated' in ch]
370
+
371
+ if additional_key_func:
372
+ key_func = lambda pair: (additional_key_func(pair[0]), pair[0])
373
+ else:
374
+ key_func = lambda pair: pair[0]
375
+
376
+ choices.sort(key=key_func)
377
+ choices.extend(deprecated_choices)
378
+
379
+ for i, (short_name, unused_choice) in enumerate(choices):
380
+ print '%d: %s' % (i + 1, short_name)
381
+
382
+ selection = self._ReadInSelectedItem(
383
+ range(1, len(choices) + 1), collection_name + 's')
384
+ return choices[selection - 1][1]
385
+
386
+ def _PromptForKernel(self):
387
+ """Prompt the user to select a kernel from the available kernels.
388
+
389
+ Returns:
390
+ A kernel resource selected by the user, or None if no kernels available.
391
+ """
392
+
393
+ def ExtractKernelPrompt(kernel):
394
+ return self._PresentElement(
395
+ self.NormalizeGlobalResourceName('google', 'kernels', kernel['name']))
396
+
397
+ return self._PromptForEntry(
398
+ self._kernels_api, 'kernel', 'google',
399
+ extract_resource_prompt=ExtractKernelPrompt)
400
+
401
+ def _PromptForImage(self):
402
+ choices = (utils.All(self._images_api.list, 'google')['items'] +
403
+ utils.All(self._images_api.list, self._project)['items'])
404
+
405
+ def ExtractImagePrompt(image):
406
+ return self._PresentElement(image['selfLink'])
407
+
408
+ return self._PromptForChoice(choices, 'image', True, ExtractImagePrompt)
409
+
410
+ def _PromptForZone(self):
411
+ """Prompt the user to select a zone from the current list.
412
+
413
+ Returns:
414
+ A zone resource as selected by the user.
415
+ """
416
+ now = datetime.datetime.utcnow()
417
+
418
+ def ExtractZonePrompt(zone):
419
+ """Creates a text prompt for a zone resource.
420
+
421
+ Includes maintenance information for zones that enter maintenance in less
422
+ than two weeks.
423
+
424
+ Args:
425
+ zone: The Google Compute Engine zone resource.
426
+
427
+ Returns:
428
+ string to represent a specific zone choice to present to the user.
429
+ """
430
+ name = zone['name'].split('/')[-1]
431
+ maintenance = GoogleComputeCommand._GetNextMaintenanceStart(zone, now)
432
+ if maintenance is not None:
433
+ if maintenance < now:
434
+ msg = 'currently in maintenance'
435
+ else:
436
+ delta = maintenance - now
437
+ if delta >= datetime.timedelta(weeks=2):
438
+ msg = None
439
+ elif delta.days < 1:
440
+ msg = 'maintenance starts in less than 24 hours'
441
+ elif delta.days == 1:
442
+ msg = 'maintenance starts in 1 day'
443
+ else:
444
+ msg = 'maintenance starts in %s days' % delta.days
445
+ if msg:
446
+ return '%s (%s)' % (name, msg)
447
+ return name
448
+
449
+ return self._PromptForEntry(self._zones_api, 'zone',
450
+ extract_resource_prompt=ExtractZonePrompt)
451
+
452
+ def _PromptForDisk(self):
453
+ """Prompt the user to select a disk from the current list.
454
+
455
+ Returns:
456
+ A disk resource as selected by the user.
457
+ """
458
+ return self._PromptForEntry(self._disks_api, 'disk', auto_select=False)
459
+
460
+ def _GetMachineTypeSecondarySortScore(self, value):
461
+ """Returns a score for the given machine type to be used in sorting.
462
+
463
+ This is used to ensure that the lower cost machine types are the
464
+ first ones displayed to the user.
465
+
466
+ Args:
467
+ value: The name of a machine type.
468
+
469
+ Returns:
470
+ An integer that defines a sort order.
471
+ """
472
+ for i in range(len(MACHINE_TYPE_ORDERING)):
473
+ if MACHINE_TYPE_ORDERING[i] in value:
474
+ return i
475
+ return len(MACHINE_TYPE_ORDERING)
476
+
477
+ def _PromptForMachineType(self):
478
+ """Prompt the user to select a machine type from the current list.
479
+
480
+ Returns:
481
+ A machine type resource as selected by the user.
482
+ """
483
+ return self._PromptForEntry(
484
+ self._machine_types_api, 'machine type',
485
+ additional_key_func=self._GetMachineTypeSecondarySortScore)
486
+
487
+ @staticmethod
488
+ def _GetNextMaintenanceStart(zone, now=None):
489
+ def ParseDate(date):
490
+ # Removes the timezone awareness from the timestamp we get back
491
+ # from the server. This is necessary because utcnow() is
492
+ # timezone unaware and it's much easier to remove timezone
493
+ # awareness than to add it in. The latter option requires more
494
+ # code and possibly other libraries.
495
+ return iso8601.parse_date(date).replace(tzinfo=None)
496
+
497
+ if now is None:
498
+ now = datetime.datetime.utcnow()
499
+ maintenance = zone.get('maintenanceWindows')
500
+ next_window = None
501
+ if maintenance:
502
+ # Find the next maintenance window.
503
+ for mw in maintenance:
504
+ # Is it already past?
505
+ end = mw.get('endTime')
506
+ if end:
507
+ end = ParseDate(end)
508
+ if end < now:
509
+ # Skip maintenance because it has occurred in the past.
510
+ continue
511
+
512
+ begin = mw.get('beginTime')
513
+ if begin:
514
+ begin = ParseDate(begin)
515
+ if next_window is None or begin < next_window:
516
+ next_window = begin
517
+ return next_window
518
+
519
+ def _GetZone(self, zone=None):
520
+ """Notifies the user if the given zone will enter maintenance soon.
521
+
522
+ The given zone can be None in which case the user is prompted for
523
+ a zone. This method is intended to provide a warning to the user
524
+ if he or she seeks to create a disk or instance in a zone that
525
+ will enter maintenance in less than two weeks.
526
+
527
+ Args:
528
+ zone: The name of the zone chosen, or None.
529
+
530
+ Returns:
531
+ The given zone or the zone chosen through the prompt.
532
+ """
533
+ if zone is None:
534
+ zone_resource = self._PromptForZone()
535
+ zone = zone_resource['name']
536
+ else:
537
+ zone = zone.split('/')[-1]
538
+ zone_resource = self._zones_api.get(
539
+ project=self._project, zone=zone).execute()
540
+
541
+ # Warns the user if there is an upcoming maintenance for the
542
+ # chosen zone. Times returned from the server are in UTC.
543
+ now = datetime.datetime.utcnow()
544
+ next_win = GoogleComputeCommand._GetNextMaintenanceStart(
545
+ zone_resource, now)
546
+ if next_win is not None:
547
+ if next_win < now:
548
+ msg = 'is unavailable due to maintenance'
549
+ else:
550
+ delta = next_win - now
551
+ if delta >= datetime.timedelta(weeks=2):
552
+ msg = None
553
+ elif delta.days < 1:
554
+ msg = 'less than 24 hours'
555
+ elif delta.days == 1:
556
+ msg = '1 day'
557
+ else:
558
+ msg = '%s days' % delta.days
559
+ if msg:
560
+ msg = 'will become unavailable due to maintenance in %s' % msg
561
+ if msg:
562
+ LOGGER.warn('%s %s.', zone, msg)
563
+ return zone
564
+
565
+ def _GetZones(self):
566
+ """Retrieves the full list of zones available to this project.
567
+
568
+ Returns:
569
+ List of zones available to this project.
570
+ """
571
+ return utils.AllNames(self._zones_api.list, self._project)
572
+
573
+ def _AuthenticateWrapper(self, http):
574
+ """Adds the OAuth token into http request.
575
+
576
+ Args:
577
+ http: An instance of httplib2.Http or something that acts like it.
578
+
579
+ Returns:
580
+ httplib2.Http like object.
581
+
582
+ Raises:
583
+ CommandError: If the credentials can't be found.
584
+ """
585
+ if not self._credential:
586
+ self._credential = auth_helper.GetCredentialFromStore(
587
+ self.__GetRequiredAuthScopes())
588
+ if not self._credential:
589
+ raise CommandError(
590
+ 'Could not get valid credentials for API.')
591
+ return self._credential.authorize(http)
592
+
593
+
594
+ def _ParseArgumentsAndFlags(self, flag_values, argv):
595
+ """Parses the command line arguments for the command.
596
+
597
+ This method matches up positional arguments based on the
598
+ signature of the Handle method. It also parses the flags
599
+ found on the command line.
600
+
601
+ argv will contain, <main python file>, positional-arguments, flags...
602
+
603
+ Args:
604
+ flag_values: The flags list to update
605
+ argv: The command line argument list
606
+
607
+ Returns:
608
+ The list of position arguments for the given command.
609
+
610
+ Raises:
611
+ CommandError: If any problems occur with parsing the commands (e.g.,
612
+ type mistmatches, out of bounds, unknown commands, ...).
613
+ """
614
+ # If we are sorting args and flags, kick the flag parser into gnu
615
+ # mode and parse some more. argv will be all of the unparsed args
616
+ # after this.
617
+ if self.sort_args_and_flags:
618
+ try:
619
+ old_gnu_mode = flag_values.IsGnuGetOpt()
620
+ flag_values.UseGnuGetOpt(True)
621
+ argv = flag_values(argv)
622
+ except (flags.IllegalFlagValue, flags.UnrecognizedFlagError) as e:
623
+ raise CommandError(e)
624
+ finally:
625
+ flag_values.UseGnuGetOpt(old_gnu_mode)
626
+
627
+ # We use the same positional arguments used by the command's Handle method.
628
+ # For AddDisk this will be, ['self', 'disk_name'].
629
+ argspec = inspect.getargspec(self.Handle)
630
+
631
+ # Skip the implicit argument 'self' and take the list of
632
+ # positional command args.
633
+ default_count = len(argspec.defaults) if argspec.defaults else 0
634
+ pos_arg_names = argspec.args[1:]
635
+
636
+ # We then parse off values for those positional arguments from argv.
637
+ # Note that we skip the first argument, as that is the command path.
638
+ pos_arg_values = argv[1:len(pos_arg_names) + 1]
639
+
640
+ # Take all the arguments past the positional arguments. If there
641
+ # is a var_arg on the command this will get passed in.
642
+ unparsed_args = argv[len(pos_arg_names) + 1:]
643
+
644
+ # If we did not get enough positional argument values print error and exit.
645
+ if len(pos_arg_names) - default_count > len(pos_arg_values):
646
+ missing_args = pos_arg_names[len(pos_arg_values):]
647
+ missing_args = ['"%s"' % a for a in missing_args]
648
+ raise CommandError('Positional argument %s is missing.' %
649
+ ', '.join(missing_args))
650
+
651
+ # If users specified flags in place of positional argument values,
652
+ # print error and exit.
653
+ for (name, value) in zip(pos_arg_names, pos_arg_values):
654
+ if value.startswith('--'):
655
+ raise CommandError('Invalid positional argument value \'%s\' '
656
+ 'for argument \'%s\'\n' % (value, name))
657
+
658
+ # If there are any unparsed args and the command is not expecting
659
+ # varargs, print error and exit.
660
+ if (unparsed_args and
661
+
662
+ # MOE_begin_strip
663
+ # This is a temporary measure to allow new-style commands to
664
+ # have varargs without having a Handle method.
665
+ # MOE_end_strip
666
+ not getattr(self, 'has_varargs', False) and
667
+
668
+ not argspec.varargs):
669
+ unparsed_args = ['"%s"' % a for a in unparsed_args]
670
+ raise CommandError('Unknown argument: %s' %
671
+ ', '.join(unparsed_args))
672
+
673
+ return argv[1:]
674
+
675
+ def _BuildComputeApi(self, http):
676
+ """Builds the Google Compute Engine API to use.
677
+
678
+ Args:
679
+ http: a httplib2.Http like object for communication.
680
+
681
+ Returns:
682
+ The API object to use.
683
+ """
684
+ # For versions of the apiclient library prior to v1beta2, we need to
685
+ # specify the LoggingJsonModel in order to get request and response
686
+ # logging to work.
687
+ json_model = (model.LoggingJsonModel()
688
+ if 'LoggingJsonModel' in dir(model)
689
+ else model.JsonModel())
690
+ if FLAGS.fetch_discovery:
691
+ discovery_uri = (FLAGS.api_host +
692
+ 'discovery/v1/apis/{api}/{apiVersion}/rest')
693
+ return self.WrapApiIfNeeded(discovery.build(
694
+ 'compute',
695
+ FLAGS.service_version,
696
+ http=http,
697
+ discoveryServiceUrl=discovery_uri,
698
+ model=json_model))
699
+ else:
700
+ discovery_file_name = os.path.join(
701
+ os.path.dirname(__file__),
702
+ 'compute/%s.json' % FLAGS.service_version)
703
+ try:
704
+ discovery_file = file(discovery_file_name, 'r')
705
+ discovery_doc = discovery_file.read()
706
+ discovery_file.close()
707
+ except IOError:
708
+ raise CommandError(
709
+ 'Could not load discovery document from disk. Perhaps try '
710
+ '--fetch_discovery. \nFile: %s' % discovery_file_name)
711
+
712
+ return self.WrapApiIfNeeded(discovery.build_from_document(
713
+ discovery_doc,
714
+ base=FLAGS.api_host,
715
+ http=http,
716
+ model=json_model))
717
+
718
+ @staticmethod
719
+ def WrapApiIfNeeded(api):
720
+ """Wraps the API to enable logging or tracing."""
721
+ if FLAGS.trace_token:
722
+ return TracedComputeApi(api, 'token:%s' % (FLAGS.trace_token))
723
+ return api
724
+
725
+ @staticmethod
726
+ def DenormalizeResourceName(resource_name):
727
+ """Return the relative name for the given resource.
728
+
729
+ Args:
730
+ resource_name: The name of the resource. This can be either relative or
731
+ absolute.
732
+
733
+ Returns:
734
+ The name of the resource relative to its enclosing collection.
735
+ """
736
+ return resource_name.strip('/').rpartition('/')[2]
737
+
738
+ @staticmethod
739
+ def DenormalizeProjectName(flag_values):
740
+ """Denormalize the 'project' entry in the given FlagValues instance.
741
+
742
+ Args:
743
+ flag_values: The FlagValues instance to update.
744
+
745
+ Raises:
746
+ CommandError: If the project is missing or malformed.
747
+ """
748
+ project = flag_values.project or flag_values.project_id
749
+
750
+ if not project:
751
+ raise CommandError(
752
+ 'You must specify a project name using the "--project" flag.')
753
+ elif project.lower() != project:
754
+ raise CommandError(
755
+ 'Characters in project name must be lowercase: %s.' % project)
756
+
757
+ project = project.strip('/')
758
+ if project.startswith('projects/'):
759
+ project = project[len('projects/'):]
760
+ if '/' in project:
761
+ raise CommandError('Project names can contain a \'/\' only when they '
762
+ 'begin with \'projects/\'.')
763
+
764
+ flag_values.project = project
765
+ flag_values.project_id = None
766
+
767
+ def _GetBaseApiUrl(self):
768
+ """Get the base API URL given the current flag_values.
769
+
770
+ Returns:
771
+ The base API URL. For example,
772
+ https://www.googleapis.com/compute/v1beta14.
773
+ """
774
+ return '%scompute/%s' % (self._flags.api_host, self._flags.service_version)
775
+
776
+ def _AddBaseUrlIfNecessary(self, resource_path):
777
+ """Add the base URL to a resource_path if required by the service_version.
778
+
779
+ Args:
780
+ resource_path: The resource path to add the URL to.
781
+
782
+ Returns:
783
+ A full API-usable reference to the given resource_path.
784
+ """
785
+ if not self._GetBaseApiUrl() in resource_path:
786
+ return '%s/%s' % (self._GetBaseApiUrl(), resource_path)
787
+ return resource_path
788
+
789
+ def _StripBaseUrl(self, value):
790
+ """Removes the a base URL from the string if it exists.
791
+
792
+ Note that right now the server may not return exactly the right
793
+ base URL so we strip off stuff that looks like a base URL.
794
+
795
+ Args:
796
+ value: The string to strip the base URL from.
797
+
798
+ Returns:
799
+ A string without the base URL.
800
+ """
801
+ pattern = '^' + re.escape(self._flags.api_host) + r'compute/\w*/'
802
+ return re.sub(pattern, '', value)
803
+
804
+ def NormalizeResourceName(self, project, scope_name, collection_name,
805
+ resource_name):
806
+ """Return the full name for the given resource.
807
+
808
+ Args:
809
+ project: The name of the project containing the resource.
810
+ scope_name: The scope of the collection containing the resource.
811
+ collection_name: The name of the collection containing the resource.
812
+ resource_name: The name of the resource. This can be either relative
813
+ or absolute.
814
+
815
+ Returns:
816
+ The full URL of the resource.
817
+ """
818
+ resource_name = resource_name.strip('/')
819
+
820
+ if (collection_name == 'machine-types' and
821
+ 'v1beta13' in self.supported_versions and
822
+ self._IsUsingAtLeastApiVersion('v1beta13')):
823
+ collection_name = 'machineTypes'
824
+
825
+ if (resource_name.startswith('projects/') or
826
+ resource_name.startswith(collection_name + '/') or
827
+ resource_name.startswith(self._flags.api_host)):
828
+ # This does not appear to be a relative name.
829
+ return self._AddBaseUrlIfNecessary(resource_name)
830
+
831
+ absolute_name = 'projects/%s/%s/%s' % (project,
832
+ collection_name,
833
+ resource_name)
834
+
835
+ if self._IsUsingAtLeastApiVersion('v1beta14') and scope_name:
836
+ absolute_name = 'projects/%s/%s/%s/%s' % (project,
837
+ scope_name,
838
+ collection_name,
839
+ resource_name)
840
+ return self._AddBaseUrlIfNecessary(absolute_name)
841
+
842
+ def NormalizeTopLevelResourceName(self, project, collection, resource):
843
+ """Return the full name for the given resource.
844
+
845
+ Args:
846
+ project: The name of the project containing the resource.
847
+ collection: The name of the collection containing the resource.
848
+ resource: The name of the resource. This can be either relative or
849
+ absolute.
850
+
851
+ Returns:
852
+ The full URL of the resource.
853
+ """
854
+ return self.NormalizeResourceName(project,
855
+ None,
856
+ collection,
857
+ resource)
858
+
859
+ def NormalizeGlobalResourceName(self, project, collection, resource):
860
+ """Return the full name for the given resource.
861
+
862
+ Args:
863
+ project: The name of the project containing the resource.
864
+ collection: The name of the collection containing the resource.
865
+ resource: The name of the resource. This can be either relative or
866
+ absolute.
867
+
868
+ Returns:
869
+ The full URL of the resource.
870
+ """
871
+ return self.NormalizeResourceName(project,
872
+ 'global',
873
+ collection,
874
+ resource)
875
+
876
+ def NormalizePerZoneResourceName(self, project, zone, collection, resource):
877
+ """Return the full name for the given resource.
878
+
879
+ Args:
880
+ project: The name of the project containing the resource.
881
+ zone: The name of the zone containing the resource.
882
+ collection: The name of the collection containing the resource.
883
+ resource: The name of the resource. This can be either relative or
884
+ absolute.
885
+
886
+ Returns:
887
+ The full URL of the resource.
888
+ """
889
+ return self.NormalizeResourceName(project,
890
+ 'zones/%s' % zone,
891
+ collection,
892
+ resource)
893
+
894
+ def GetZoneForResource(self, api, resource_name, fail_if_not_found=True):
895
+ """Gets the unqualified zone name for a given resource.
896
+
897
+ The function first tries to use 'zone' parameter if set, but falls back
898
+ to searching for the resource name across zones.
899
+
900
+ Args:
901
+ api: The API service that must expose 'list' method.
902
+ resource_name: Name of the resource to find.
903
+ fail_if_not_found: Raise an error when the resource is not found.
904
+
905
+ Returns:
906
+ Unqualified name of the zone the resource belongs to.
907
+
908
+ Raises:
909
+ CommandError: If the zone for the resource cannot be resolved.
910
+ """
911
+ # If the resource is already project- and zone-qualified, use the zone.
912
+ if not resource_name:
913
+ return None
914
+
915
+ resource_name_parts = self._StripBaseUrl(resource_name).split('/')
916
+ if (len(resource_name_parts) > 3 and
917
+ resource_name_parts[0] == 'projects' and
918
+ resource_name_parts[2] == 'zones'):
919
+ return resource_name_parts[3]
920
+
921
+ if self._flags.zone == GLOBAL_ZONE_NAME:
922
+ return None
923
+
924
+ if self._flags.zone:
925
+ return self._flags.zone
926
+
927
+ filter_expression = utils.RegexesToFilterExpression(
928
+ [self.DenormalizeResourceName(resource_name)])
929
+
930
+ items = []
931
+ for zone in self._GetZones():
932
+ # Limiting the number of results to 2, since anything other than one
933
+ # is an error.
934
+ sub_result = utils.All(api.list,
935
+ self._project,
936
+ max_results=2,
937
+ filter=filter_expression,
938
+ zone=zone)
939
+ items.extend(sub_result.get('items', []))
940
+
941
+ if len(items) == 1:
942
+ zone = self._GetZoneFromSelfLink(items[0]['selfLink'])
943
+ LOGGER.info('Zone for %s detected as %s.', repr(resource_name),
944
+ repr(zone or GLOBAL_ZONE_NAME))
945
+ LOGGER.warning('Consider passing \'--zone=%s\' to avoid the unnecessary '
946
+ 'zone lookup which requires extra API calls.',
947
+ zone or GLOBAL_ZONE_NAME)
948
+ return zone
949
+
950
+ if fail_if_not_found:
951
+ raise CommandError('Could not determine the zone of \'%s\'.' %
952
+ resource_name)
953
+ else:
954
+ return None
955
+
956
+ def _GetZoneFromSelfLink(self, self_link):
957
+ """Parses the given self-link and returns per-project zone name."""
958
+ resource_name = self._StripBaseUrl(self_link)
959
+ parts = resource_name.split('/')
960
+ if len(parts) > 3 and parts[0] == 'projects' and parts[2] == 'zones':
961
+ return parts[3]
962
+ else:
963
+ return None
964
+
965
+ def _HandleSafetyPrompt(self, positional_arguments):
966
+ """If a safety prompt is present on the class, handle it now.
967
+
968
+ By defining a field 'safety_prompt', derived classes can request
969
+ that the user confirm a dangerous operation prior to execution,
970
+ e.g. deleting a resource. Users may override this check by
971
+ passing the --force flag on the command line.
972
+
973
+ Args:
974
+ positional_arguments: A list of positional argument strings.
975
+
976
+ Returns:
977
+ True if the command should continue, False if not.
978
+ """
979
+ if hasattr(self, 'safety_prompt'):
980
+ if not self._flags.force:
981
+ prompt = self.safety_prompt
982
+ if positional_arguments:
983
+ prompt = '%s %s' % (prompt, ', '.join(positional_arguments))
984
+ print '%s? [y/N]' % prompt
985
+ userinput = raw_input('>>> ')
986
+
987
+ if not userinput:
988
+ userinput = 'n'
989
+ userinput = userinput.lstrip()[:1].lower()
990
+
991
+ if not userinput == 'y':
992
+ return False
993
+
994
+ return True
995
+
996
+ def _IsUsingAtLeastApiVersion(self, required_version):
997
+ """Determine if in-use API version is at least the specified version.
998
+
999
+ Args:
1000
+ required_version: The API version to test.
1001
+
1002
+ Returns:
1003
+ True if the given API version is equal or newer than the in-use
1004
+ API version, False otherwise.
1005
+
1006
+ Raises:
1007
+ CommandError: If the specified API version is not known.
1008
+ """
1009
+ if not (required_version in self.supported_versions and
1010
+ self._flags.service_version in self.supported_versions):
1011
+ raise CommandError('API version %s/%s unknown' % (
1012
+ required_version, self._flags.service_version))
1013
+
1014
+ for index, known_version in enumerate(self.supported_versions):
1015
+ if known_version == self._flags.service_version:
1016
+ current_index = index
1017
+ if known_version == required_version:
1018
+ given_index = index
1019
+
1020
+ return current_index >= given_index
1021
+
1022
+ def _GetResourceApiKind(self, resource):
1023
+ """Determine the API version driven resource 'kind'.
1024
+
1025
+ Args:
1026
+ resource: The resource type to generate a 'kind' string for.
1027
+
1028
+ Returns:
1029
+ A string containing the API 'kind'
1030
+ """
1031
+ return 'compute#%s' % resource
1032
+
1033
+ def _ErrorInResult(self, result):
1034
+ """Return True if a result should be considered an error."""
1035
+ ops = []
1036
+ if self.IsResultAnOperation(result):
1037
+ ops = [result]
1038
+ elif self.IsResultAList(result):
1039
+ ops = result.get('items', [])
1040
+ for op in ops:
1041
+ # If op contains errors, it will be of the form:
1042
+ # {'error': {'errors': [...]}, ...}
1043
+ if (self._flags.synchronous_mode and
1044
+ op.get('error', {}).get('errors', [])):
1045
+ return True
1046
+ return False
1047
+
1048
+ def Run(self, argv):
1049
+ """Run the command, printing the result.
1050
+
1051
+ Args:
1052
+ argv: The arguments to the command.
1053
+
1054
+ Returns:
1055
+ 0 if the command completes successfully, otherwise 1.
1056
+ """
1057
+ try:
1058
+ pos_arg_values = self._ParseArgumentsAndFlags(FLAGS, argv)
1059
+ gcutil_logging.SetupLogging()
1060
+
1061
+ # Synchronize the flags with any cached values present.
1062
+ flags_cache_obj = flags_cache.FlagsCache()
1063
+ flags_cache_obj.SynchronizeFlags()
1064
+
1065
+
1066
+ self.SetFlagDefaults()
1067
+ self.DenormalizeProjectName(FLAGS)
1068
+ self.SetFlags(FLAGS)
1069
+
1070
+ auth_retry = True
1071
+ error_in_result = False
1072
+
1073
+ while auth_retry:
1074
+ try:
1075
+ result, exceptions = self.RunWithFlagsAndPositionalArgs(
1076
+ self._flags, pos_arg_values)
1077
+ auth_retry = False
1078
+
1079
+ self.PrintResult(result)
1080
+ self.LogExceptions(exceptions)
1081
+
1082
+ if self._ErrorInResult(result):
1083
+ error_in_result = True
1084
+
1085
+ # If we just have an AccessTokenRefreshError raise it so
1086
+ # that we retry.
1087
+ for exception in exceptions:
1088
+ if isinstance(exception, oauth2_client.AccessTokenRefreshError):
1089
+ if not result:
1090
+ raise exception
1091
+ else:
1092
+ LOGGER.warning('Refresh error when running multiple '
1093
+ 'operations. Not automatically retrying as '
1094
+ 'some requests succeeded.')
1095
+ break
1096
+
1097
+ except oauth2_client.AccessTokenRefreshError, e:
1098
+ if not auth_retry:
1099
+ raise
1100
+ # Retrying the operation will induce OAuth2 reauthentication and
1101
+ # creation of the new refresh token.
1102
+ LOGGER.info('OAuth2 token refresh error (%s), retrying.\n', str(e))
1103
+ auth_retry = False
1104
+
1105
+ has_errors = bool(exceptions or error_in_result)
1106
+
1107
+ # Updates the flags cache file only when the command exits with
1108
+ # a non-zero error code.
1109
+ if not has_errors:
1110
+ flags_cache_obj.UpdateCacheFile()
1111
+
1112
+ return has_errors
1113
+ except errors.HttpError, http_error:
1114
+ self.LogHttpError(http_error)
1115
+ return 1
1116
+ except app.UsageError:
1117
+ raise
1118
+ except:
1119
+ sys.stderr.write('%s\n' % '\n'.join(
1120
+ traceback.format_exception_only(sys.exc_type, sys.exc_value)))
1121
+ LOGGER.debug(traceback.format_exc())
1122
+ return 1
1123
+
1124
+ def CreateHttp(self):
1125
+ """Construct an HTTP object to use with an API call.
1126
+
1127
+ This is useful when doing multithreaded work as httplib2 Http
1128
+ objects aren't threadsafe.
1129
+
1130
+ Returns:
1131
+ An object that implements the httplib2.Http interface
1132
+ """
1133
+ http = httplib2.Http()
1134
+ http = self._AuthenticateWrapper(http)
1135
+ return http
1136
+
1137
+ def RunWithFlagsAndPositionalArgs(self, flag_values, pos_arg_values):
1138
+ """Run the command with the parsed flags and positional arguments.
1139
+
1140
+ This method is what a subclass should override if they do not want
1141
+ to use the REST API.
1142
+
1143
+ Args:
1144
+ flag_values: The parsed FlagValues instance.
1145
+ pos_arg_values: The positional arguments for the Handle method.
1146
+
1147
+ Raises:
1148
+ CommandError: If user choses to not proceed with the command at safety
1149
+ prompt.
1150
+
1151
+ Returns:
1152
+ A tuple (result, exceptions) where results is a
1153
+ JSON-serializable result and exceptions is a list of exceptions
1154
+ that were thrown when running this command.
1155
+ """
1156
+ http = self.CreateHttp()
1157
+ compute_api = self._BuildComputeApi(http)
1158
+ if self._IsUsingAtLeastApiVersion('v1beta14'):
1159
+ self._zone_operations_api = compute_api.zoneOperations()
1160
+ self._global_operations_api = compute_api.globalOperations()
1161
+ else:
1162
+ self._global_operations_api = compute_api.operations()
1163
+
1164
+ self.SetApi(compute_api)
1165
+
1166
+ if not self._HandleSafetyPrompt(pos_arg_values):
1167
+ raise CommandError('Operation aborted')
1168
+
1169
+ exceptions = []
1170
+ result = self.Handle(*pos_arg_values)
1171
+ if isinstance(result, tuple):
1172
+ result, exceptions = result
1173
+ if self._flags.synchronous_mode:
1174
+ result = self.WaitForOperation(flag_values, time, result)
1175
+ if isinstance(result, list):
1176
+ result = self.MakeListResult(result, 'operationList')
1177
+
1178
+ return result, exceptions
1179
+
1180
+ def IsResultAnOperation(self, result):
1181
+ """Determine if the result object is an operation."""
1182
+ try:
1183
+ return ('kind' in result
1184
+ and result['kind'].endswith('#operation'))
1185
+ except TypeError:
1186
+ return False
1187
+
1188
+ def IsResultAList(self, result):
1189
+ """Determine if the result object is a list of some sort."""
1190
+ try:
1191
+ return ('kind' in result
1192
+ and result['kind'].endswith('List'))
1193
+ except TypeError:
1194
+ return False
1195
+
1196
+ def MakeListResult(self, results, kind_base):
1197
+ """Given an array of results, create an list object for those results.
1198
+
1199
+ Args:
1200
+ results: The list of results.
1201
+ kind_base: The kind of list to create
1202
+
1203
+ Returns:
1204
+ A synthetic list resource created from the list of individual results.
1205
+ """
1206
+ return {
1207
+ 'kind': self._GetResourceApiKind(kind_base),
1208
+ 'items': results,
1209
+ 'note': ('This JSON result is based on multiple API calls. This '
1210
+ 'object was created in the client.')
1211
+ }
1212
+
1213
+ def ExecuteRequests(self, requests, collection_name=None):
1214
+ """Execute a list of requests in a thread pool.
1215
+
1216
+ Args:
1217
+ requests: A list of requests objects to execute.
1218
+ collection_name: The name of the collection. This is optional and is
1219
+ useful for subclasses that mutate more than one resource type.
1220
+
1221
+ Returns:
1222
+ A tuple with (results, exceptions) where result list is the list
1223
+ of all results and exceptions is any exceptions that were
1224
+ raised.
1225
+ """
1226
+ tp = thread_pool.ThreadPool(self._flags.concurrent_operations)
1227
+ ops = []
1228
+ for request in requests:
1229
+ op = ApiThreadPoolOperation(
1230
+ request, self, self._flags.synchronous_mode,
1231
+ collection_name=collection_name)
1232
+ ops.append(op)
1233
+ tp.Add(op)
1234
+ tp.WaitShutdown()
1235
+ results = []
1236
+ exceptions = []
1237
+ for op in ops:
1238
+ if op.RaisedException():
1239
+ exceptions.append(op.Result())
1240
+ else:
1241
+ if isinstance(op.Result(), list):
1242
+ results.extend(op.Result())
1243
+ else:
1244
+ results.append(op.Result())
1245
+ return (results, exceptions)
1246
+
1247
+ def WaitForOperation(self, flag_values, timer, result, http=None,
1248
+ collection_name=None):
1249
+ """Wait for a potentially asynchronous operation to complete.
1250
+
1251
+ Args:
1252
+ flag_values: The parsed FlagValues instance.
1253
+ timer: An implementation of the time object, providing time and sleep
1254
+ methods.
1255
+ result: The result of the request, potentially containing an operation.
1256
+ http: An optional httplib2.Http object to use for requests.
1257
+
1258
+ Returns:
1259
+ The synchronous return value, usually an operation object.
1260
+ """
1261
+ resource = None
1262
+ if not self.IsResultAnOperation(result):
1263
+ return result
1264
+
1265
+ start_time = timer.time()
1266
+ operation_type = result['operationType']
1267
+ target = result['targetLink'].split('/')[-1]
1268
+
1269
+ while result['status'] != 'DONE':
1270
+ if timer.time() - start_time >= flag_values.max_wait_time:
1271
+ LOGGER.warn('Timeout reached. %s of %s has not yet completed. '
1272
+ 'The operation (%s) is still %s.',
1273
+ operation_type, target, result['name'], result['status'])
1274
+ break # Timeout
1275
+
1276
+ collection_name = (collection_name
1277
+ or getattr(self, 'resource_collection_name', None))
1278
+ if collection_name:
1279
+ singular_collection_name = utils.Singularize(collection_name)
1280
+ qualified_name = '%s %s' % (singular_collection_name, target)
1281
+ else:
1282
+ qualified_name = target
1283
+
1284
+ LOGGER.info('Waiting for %s of %s. Sleeping for %ss.', operation_type,
1285
+ qualified_name, flag_values.sleep_between_polls)
1286
+ timer.sleep(flag_values.sleep_between_polls)
1287
+
1288
+ kwargs = {
1289
+ 'project': self._project,
1290
+ 'operation': result['name'],
1291
+ }
1292
+
1293
+ poll_api = self._global_operations_api
1294
+
1295
+ if self._IsUsingAtLeastApiVersion('v1beta14'):
1296
+ operation_zone = self._GetZoneFromSelfLink(result['selfLink'])
1297
+ if operation_zone:
1298
+ kwargs['zone'] = operation_zone
1299
+ poll_api = self._zone_operations_api
1300
+
1301
+ # Poll the operation for status.
1302
+ request = poll_api.get(**kwargs)
1303
+ result = request.execute(http=http)
1304
+ else:
1305
+ if result['operationType'] != 'delete' and 'error' not in result:
1306
+ # We are going to replace the operation with its resulting resource.
1307
+ # Save the operation to return as well.
1308
+ target_link = result['targetLink']
1309
+ http = self.CreateHttp()
1310
+ response, data = http.request(target_link, method='GET')
1311
+ if 200 <= response.status <= 299:
1312
+ resource = json.loads(data)
1313
+
1314
+ if resource is not None:
1315
+ results = []
1316
+ results.append(result)
1317
+ results.append(resource)
1318
+ return results
1319
+ return result
1320
+
1321
+ def CommandGetHelp(self, unused_argv, cmd_names=None):
1322
+ """Get help for command.
1323
+
1324
+ Args:
1325
+ unused_argv: Remaining command line flags and arguments after parsing
1326
+ command (that is a copy of sys.argv at the time of the
1327
+ function call with all parsed flags removed); unused in this
1328
+ implementation.
1329
+ cmd_names: By default, if help is being shown for more than one command,
1330
+ and this command defines _all_commands_help, then
1331
+ _all_commands_help will be displayed instead of the class
1332
+ doc. cmd_names is used to determine the number of commands
1333
+ being displayed and if only a single command is display then
1334
+ the class doc is returned.
1335
+
1336
+ Returns:
1337
+ __doc__ property for command function or a message stating there is no
1338
+ help.
1339
+ """
1340
+ help_str = super(
1341
+ GoogleComputeCommand, self).CommandGetHelp(unused_argv, cmd_names)
1342
+ return '%s\n\nUsage: %s' % (help_str, self._GetUsage())
1343
+
1344
+ def _GetUsage(self):
1345
+ """Get the usage string for the command, used to print help messages.
1346
+
1347
+ Returns:
1348
+ The usage string for the command.
1349
+ """
1350
+ res = '%s [--global_flags] %s [--command_flags]' % (
1351
+ os.path.basename(sys.argv[0]), self._command_name)
1352
+
1353
+ args = getattr(self, 'positional_args', None)
1354
+ if args:
1355
+ res = '%s %s' % (res, args)
1356
+
1357
+ return res
1358
+
1359
+ def Handle(self):
1360
+ """Actual implementation of the command.
1361
+
1362
+ Derived classes override this method, adding positional arguments
1363
+ to this method as required.
1364
+
1365
+ Returns:
1366
+ Either a single JSON-serializable result or a tuple of a result
1367
+ and a list of exceptions that are thrown.
1368
+ """
1369
+ raise NotImplementedError()
1370
+
1371
+ def SetFlags(self, flag_values):
1372
+ """Set the flags to be used by the command.
1373
+
1374
+ Args:
1375
+ flag_values: The parsed flags values.
1376
+ """
1377
+ self._flags = flag_values
1378
+ self._project = self._flags.project
1379
+
1380
+ def GetFlags(self):
1381
+ """Get the flags object used by the command."""
1382
+ return self._flags
1383
+
1384
+ def SetApi(self, api):
1385
+ """Set the Google Compute Engine API for the command.
1386
+
1387
+ Derived classes override this method, pulling the necessary
1388
+ domain specific API out of the global API.
1389
+
1390
+ Args:
1391
+ api: The Google Compute Engine API used by this command.
1392
+ """
1393
+ raise NotImplementedError()
1394
+
1395
+ def _PresentElement(self, field_value):
1396
+ """Format a json value for tabular display.
1397
+
1398
+ Strips off the project qualifier if present and elides the value
1399
+ if it won't fit inside of a max column size of 64 characters.
1400
+
1401
+ Args:
1402
+ field_value: The json field value to be formatted.
1403
+
1404
+ Returns:
1405
+ The formatted json value.
1406
+ """
1407
+ if isinstance(field_value, basestring):
1408
+ field_value = self._StripBaseUrl(field_value).strip('/')
1409
+
1410
+ if field_value.startswith('projects/' + self._project):
1411
+ field_value_parts = field_value.split('/')
1412
+ if len(field_value_parts) > 3:
1413
+ field_value = '/'.join(field_value_parts[3:])
1414
+ else:
1415
+ field_value = field_value_parts[-1]
1416
+ if (self._flags.long_values_display_format == 'elided' and
1417
+ len(field_value) > 64):
1418
+ return field_value[:31] + '..' + field_value[-31:]
1419
+ return field_value
1420
+
1421
+ def _FlattenObjectToList(self, instance_json, name_map):
1422
+ """Convert a json instance to a dictionary for output.
1423
+
1424
+ Args:
1425
+ instance_json: A JSON object represented as a python dict.
1426
+ name_map: A list of key, json-path object tuples where the
1427
+ json-path object is either a string or a list of strings.
1428
+ ('name', 'container.id') or
1429
+ ('name', ['container.id.new', 'container.id.old'])
1430
+
1431
+ Returns:
1432
+ A list of extracted values selected by the associated JSON path. In
1433
+ addition, names are simplified to their shortest path components.
1434
+ """
1435
+
1436
+ def ExtractSubKeys(json_object, subkey):
1437
+ """Extract and flatten a (possibly-repeated) field in a json object.
1438
+
1439
+ Args:
1440
+ json_object: A JSON object represented as a python dict.
1441
+ subkey: a list of path elements, e.g. ['container', 'id'].
1442
+
1443
+ Returns:
1444
+ [element1, element2, ...] or [] if the subkey could not be found.
1445
+ """
1446
+ if not subkey:
1447
+ return [self._PresentElement(json_object)]
1448
+ if subkey[0] in json_object:
1449
+ element = json_object[subkey[0]]
1450
+ if isinstance(element, list):
1451
+ return sum([ExtractSubKeys(x, subkey[1:]) for x in element], [])
1452
+ return ExtractSubKeys(element, subkey[1:])
1453
+ return []
1454
+
1455
+ ret = []
1456
+ for unused_key, paths in name_map:
1457
+ # There may be multiple possible paths indicating the field name due to
1458
+ # versioning changes. Walk through them in order until one is found.
1459
+ if isinstance(paths, basestring):
1460
+ elements = ExtractSubKeys(instance_json, paths.split('.'))
1461
+ else:
1462
+ for path in paths:
1463
+ elements = ExtractSubKeys(instance_json, path.split('.'))
1464
+ if elements:
1465
+ break
1466
+
1467
+ ret.append(','.join([str(x) for x in elements]))
1468
+ return ret
1469
+
1470
+ def __AddErrorsForOperation(self, result, table):
1471
+ """Add any errors present in the operation result to the output table.
1472
+
1473
+ Args:
1474
+ result: The json dictionary returned by the server.
1475
+ table: The pretty printing table to be customized.
1476
+ """
1477
+ if 'error' in result:
1478
+ table.AddRow(('', ''))
1479
+ table.AddRow(('errors', ''))
1480
+ for error in result['error']['errors']:
1481
+ table.AddRow(('', ''))
1482
+ table.AddRow((' error', error['code']))
1483
+ table.AddRow((' message', error['message']))
1484
+
1485
+ def LogExceptions(self, exceptions):
1486
+ """Log a list of exceptions returned in multithreaded operation."""
1487
+ for exception in exceptions:
1488
+ if isinstance(exception, errors.HttpError):
1489
+ self.LogHttpError(exception)
1490
+ elif isinstance(exception, Exception):
1491
+ sys.stderr.write('%s\n' % '\n'.join(traceback.format_exception_only(
1492
+ type(exception).__name__, exception)))
1493
+
1494
+ def LogHttpError(self, http_error):
1495
+ """Do specific logging when we hit an HttpError."""
1496
+
1497
+ def AddMessage(messages, error):
1498
+ msg = error.get('message')
1499
+ if msg:
1500
+ messages.add(msg)
1501
+
1502
+ message = http_error.resp.reason
1503
+ try:
1504
+ data = json.loads(http_error.content)
1505
+ messages = set()
1506
+ if isinstance(data, dict):
1507
+ error = data.get('error', {})
1508
+ AddMessage(messages, error)
1509
+ for error in error.get('errors', []):
1510
+ AddMessage(messages, error)
1511
+ message = '\n'.join(messages)
1512
+ except ValueError:
1513
+ pass
1514
+
1515
+ sys.stderr.write('Error: %s\n' % message)
1516
+ # Log the full error response for debugging purposes.
1517
+ LOGGER.debug(http_error.resp)
1518
+ LOGGER.debug(http_error.content)
1519
+
1520
+ def PrintResult(self, result):
1521
+ """Pretty-print the result of the command.
1522
+
1523
+ If a class defines a list of ('title', 'json.field.path') values named
1524
+ 'fields', this list will be used to print a table of results using
1525
+ prettytable. If self.fields does not exist, result will be printed as
1526
+ pretty JSON.
1527
+
1528
+ Note that if the result is either an Operations object or an
1529
+ OperationsList, it will be special cased and formatted
1530
+ appropriately.
1531
+
1532
+ Args:
1533
+ result: A JSON-serializable object to print.
1534
+ """
1535
+ if self._flags.print_json or self._flags.format == 'json':
1536
+ # We could have used the pprint module, but it produces
1537
+ # noisy output due to all of our keys and values being
1538
+ # unicode strings rather than simply ascii.
1539
+ print json.dumps(result, sort_keys=True, indent=2)
1540
+ return
1541
+
1542
+ if result:
1543
+ if self._flags.format == 'names':
1544
+ self._PrintNamesOnly(result)
1545
+ elif self.IsResultAList(result):
1546
+ self._PrintList(result)
1547
+ else:
1548
+ self._PrintDetail(result)
1549
+
1550
+ def _PrintNamesOnly(self, result):
1551
+ """Prints only names of the resources returned by Google Compute Engine API.
1552
+
1553
+ Args:
1554
+ result: A GCE List resource to print.
1555
+ """
1556
+ if self.IsResultAList(result):
1557
+ results = result.get('items', [])
1558
+ else:
1559
+ results = [result]
1560
+
1561
+ for obj in results:
1562
+ name = obj.get('name')
1563
+ if name:
1564
+ print name
1565
+
1566
+ def _CreateFormatter(self):
1567
+ if self._flags.format == 'sparse':
1568
+ return table_formatter.SparsePrettyFormatter()
1569
+ elif self._flags.format == 'csv':
1570
+ return table_formatter.CsvFormatter()
1571
+ else:
1572
+ return table_formatter.PrettyFormatter()
1573
+
1574
+ def _PartitionResults(self, result):
1575
+ """Partitions results into operations and non-operation resources."""
1576
+ res = []
1577
+ ops = []
1578
+ for obj in result.get('items', []):
1579
+ if self.IsResultAnOperation(obj):
1580
+ ops.append(obj)
1581
+ else:
1582
+ res.append(obj)
1583
+ return res, ops
1584
+
1585
+ def _PrintList(self, result):
1586
+ """Prints a result which is a Google Compute Engine List resource.
1587
+
1588
+ For the result of batch operations, splits the result list into
1589
+ operations and other resources and possibly prints two tables. The
1590
+ operations typically represent errors (unless printing results of
1591
+ listoperations command) whereas the real resources typically
1592
+ represent successfully completed operations.
1593
+
1594
+ Args:
1595
+ result: A GCE List resource to print.
1596
+ """
1597
+ # Split results into operations and the rest of resources.
1598
+ res, ops = self._PartitionResults(result)
1599
+ if res and ops:
1600
+ res_header = '\nTable of resources:\n'
1601
+ ops_header = '\nTable of operations:\n'
1602
+ else:
1603
+ res_header = ops_header = None
1604
+
1605
+ if res or not ops:
1606
+ self._CreateAndPrintTable(res, res_header,
1607
+ getattr(self, 'summary_fields', None))
1608
+
1609
+ if ops:
1610
+ self._CreateAndPrintTable(ops, ops_header,
1611
+ self.operation_summary_fields)
1612
+
1613
+ def _CreateAndPrintTable(self, values, header, fields):
1614
+ """Creates a table representation of the list of resources and prints it.
1615
+
1616
+ Args:
1617
+ values: List of resources to display.
1618
+ header: A header to print before the table (can be None).
1619
+ fields: Summary field definition for the table.
1620
+ """
1621
+ column_names = [x[0] for x in fields]
1622
+ rows = [self._FlattenObjectToList(row, fields) for row in values]
1623
+
1624
+ table = self._CreateFormatter()
1625
+ table.AddColumns(column_names)
1626
+ table.AddRows(rows)
1627
+
1628
+ if header:
1629
+ print header
1630
+ print table
1631
+
1632
+ def _PrintDetail(self, result):
1633
+ """Prints a detail view of the result which is an individual resource.
1634
+
1635
+ Args:
1636
+ result: A resource to print.
1637
+ """
1638
+ if self.IsResultAnOperation(result):
1639
+ detail_fields = self.operation_detail_fields
1640
+ else:
1641
+ detail_fields = getattr(self, 'detail_fields', None)
1642
+
1643
+ if not detail_fields:
1644
+ return
1645
+
1646
+ row_names = [x[0] for x in detail_fields]
1647
+ table = self._CreateFormatter()
1648
+ table.AddColumns(('property', 'value'))
1649
+ property_bag = self._FlattenObjectToList(result, detail_fields)
1650
+ for i, v in enumerate(property_bag):
1651
+ table.AddRow((row_names[i], v))
1652
+
1653
+ # Handle customized printing of this result.
1654
+ # Operations are special cased here.
1655
+ if self.IsResultAnOperation(result):
1656
+ self.__AddErrorsForOperation(result, table)
1657
+ elif hasattr(self, 'CustomizePrintResult'):
1658
+ self.CustomizePrintResult(result, table)
1659
+
1660
+ print table
1661
+
1662
+ def __GetRequiredAuthScopes(self):
1663
+ """Returns a list of scopes required for this command."""
1664
+ return scopes.DEFAULT_AUTH_SCOPES
1665
+
1666
+ def SetFlagDefaults(self):
1667
+ if 'project' in FLAGS.FlagDict() and not FLAGS['project'].present:
1668
+ try:
1669
+ metadata = metadata_lib.Metadata()
1670
+ setattr(FLAGS, 'project', metadata.GetProjectId())
1671
+ except metadata_lib.MetadataError:
1672
+ pass
1673
+
1674
+
1675
+
1676
+ class GoogleComputeListCommand(GoogleComputeCommand):
1677
+ """Base class for list commands."""
1678
+
1679
+ # Overload these values in derived classes if they represent collections
1680
+ # at non-global scopes.
1681
+ is_global_level_collection = True
1682
+ is_zone_level_collection = False
1683
+
1684
+ def __init__(self, name, flag_values):
1685
+ """Initializes a new instance of a GoogleComputeListCommand.
1686
+
1687
+ Args:
1688
+ name: The name of the command.
1689
+ flag_values: The values of command line flags to be used by the command.
1690
+ """
1691
+ super(GoogleComputeListCommand, self).__init__(name, flag_values)
1692
+
1693
+ summary_fields = [x[0] for x in getattr(self, 'summary_fields', [])]
1694
+ if summary_fields:
1695
+ sort_fields = []
1696
+ for field in summary_fields:
1697
+ sort_fields.append(field)
1698
+ sort_fields.append('-' + field)
1699
+
1700
+ flags.DEFINE_enum('sort_by',
1701
+ None,
1702
+ sort_fields,
1703
+ 'Sort output results by the given field name. Field '
1704
+ 'names starting with a "-" will lead to a descending '
1705
+ 'order.',
1706
+ flag_values=flag_values)
1707
+
1708
+ flags.DEFINE_integer('max_results',
1709
+ 100,
1710
+ 'Maximum number of items to list',
1711
+ lower_bound=1,
1712
+ flag_values=flag_values)
1713
+ flags.DEFINE_string('filter',
1714
+ None,
1715
+ 'Filter expression for filtering listed resources. '
1716
+ 'See gcutil documentation for syntax of the filter '
1717
+ 'expression here: http://developers.google.com'
1718
+ '/compute/docs/gcutil/tips#filtering',
1719
+ flag_values=flag_values)
1720
+ flags.DEFINE_bool('fetch_all_pages',
1721
+ False,
1722
+ 'Whether to fetch all pages on truncated results',
1723
+ flag_values=flag_values)
1724
+
1725
+ def Handle(self):
1726
+ """Returns the result of list on a resource type."""
1727
+ if self._flags.sort_by or self._flags.fetch_all_pages:
1728
+ max_results = None
1729
+ else:
1730
+ max_results = self._flags.max_results
1731
+
1732
+ if (self._IsUsingAtLeastApiVersion('v1beta14') and
1733
+ self.is_zone_level_collection):
1734
+ # We have three cases for zone level collections:
1735
+ # 1. A specific zone was specified via flag - just list the resources
1736
+ # in that zone.
1737
+ # 2. The collection exists in both the zone and global namespaces and
1738
+ # the "global" zone was specified - just list the resources in the
1739
+ # global namespace.
1740
+ # 3. No zone was specified via flag - list all resources in all
1741
+ # namespaces for this resource type.
1742
+ if 'zone' in self._flags and self._flags.zone:
1743
+ if (self.is_global_level_collection and
1744
+ self._flags.zone == GLOBAL_ZONE_NAME):
1745
+ zones = [None]
1746
+ else:
1747
+ zones = [self.DenormalizeResourceName(self._flags.zone)]
1748
+ else:
1749
+ zones = []
1750
+ # If the collection is global and per-zone, include results from both.
1751
+ if self.is_global_level_collection:
1752
+ zones.append(None)
1753
+ zones.extend(self._GetZones())
1754
+
1755
+ items = []
1756
+ for zone in zones:
1757
+ list_func = self.ListZoneFunc() if zone else self.ListFunc()
1758
+ sub_result = utils.All(list_func,
1759
+ self._project,
1760
+ max_results,
1761
+ self._flags.filter,
1762
+ zone)
1763
+ kind = sub_result.get('kind')
1764
+ items.extend(sub_result.get('items', []))
1765
+
1766
+ return {'kind': kind, 'items': items}
1767
+
1768
+ # A global collection
1769
+ return utils.All(
1770
+ self.ListFunc(),
1771
+ self._project,
1772
+ max_results=max_results,
1773
+ filter=self._flags.filter)
1774
+
1775
+ def _PrintList(self, result):
1776
+ """Prints a table for the given resources."""
1777
+ items = result.get('items', [])
1778
+ column_names = [x[0] for x in self.summary_fields]
1779
+ rows = [self._FlattenObjectToList(row, self.summary_fields)
1780
+ for row in items]
1781
+
1782
+ sort_col = self._flags.sort_by or getattr(self, 'default_sort_field', None)
1783
+ if sort_col:
1784
+ reverse = False
1785
+ if sort_col.startswith('-'):
1786
+ reverse = True
1787
+ sort_col = sort_col[1:]
1788
+
1789
+ if sort_col in column_names:
1790
+ sort_col_idx = column_names.index(sort_col)
1791
+ rows = sorted(rows, key=(lambda row: row[sort_col_idx]),
1792
+ reverse=reverse)
1793
+ else:
1794
+ LOGGER.warn('Invalid sort column: ' + sort_col)
1795
+
1796
+ if not self._flags.fetch_all_pages:
1797
+ # Truncates the list of results. If sorting was requested, all
1798
+ # the pages had to be fetched, so we have to truncate the final
1799
+ # results on the client side. If sorting was not requested, we
1800
+ # truncate anyway in case the server gives back more results
1801
+ # than requested.
1802
+ rows = rows[:self._flags.max_results]
1803
+
1804
+ table = self._CreateFormatter()
1805
+ table.AddColumns(column_names)
1806
+ table.AddRows(rows)
1807
+
1808
+ print table