gcloud 0.0.2 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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