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.
- data.tar.gz.sig +2 -3
- data/CHANGELOG +4 -0
- data/LICENSE +674 -0
- data/Manifest +111 -0
- data/README.md +4 -3
- data/bin/gcutil +53 -0
- data/gcloud.gemspec +4 -3
- data/packages/gcutil-1.7.1/CHANGELOG +197 -0
- data/packages/gcutil-1.7.1/LICENSE +202 -0
- data/packages/gcutil-1.7.1/VERSION +1 -0
- data/packages/gcutil-1.7.1/gcutil +53 -0
- data/packages/gcutil-1.7.1/lib/google_api_python_client/LICENSE +23 -0
- data/packages/gcutil-1.7.1/lib/google_api_python_client/apiclient/__init__.py +1 -0
- data/packages/gcutil-1.7.1/lib/google_api_python_client/apiclient/discovery.py +743 -0
- data/packages/gcutil-1.7.1/lib/google_api_python_client/apiclient/errors.py +123 -0
- data/packages/gcutil-1.7.1/lib/google_api_python_client/apiclient/ext/__init__.py +0 -0
- data/packages/gcutil-1.7.1/lib/google_api_python_client/apiclient/http.py +1443 -0
- data/packages/gcutil-1.7.1/lib/google_api_python_client/apiclient/mimeparse.py +172 -0
- data/packages/gcutil-1.7.1/lib/google_api_python_client/apiclient/model.py +385 -0
- data/packages/gcutil-1.7.1/lib/google_api_python_client/apiclient/schema.py +303 -0
- data/packages/gcutil-1.7.1/lib/google_api_python_client/oauth2client/__init__.py +1 -0
- data/packages/gcutil-1.7.1/lib/google_api_python_client/oauth2client/anyjson.py +32 -0
- data/packages/gcutil-1.7.1/lib/google_api_python_client/oauth2client/appengine.py +528 -0
- data/packages/gcutil-1.7.1/lib/google_api_python_client/oauth2client/client.py +1139 -0
- data/packages/gcutil-1.7.1/lib/google_api_python_client/oauth2client/clientsecrets.py +105 -0
- data/packages/gcutil-1.7.1/lib/google_api_python_client/oauth2client/crypt.py +244 -0
- data/packages/gcutil-1.7.1/lib/google_api_python_client/oauth2client/django_orm.py +124 -0
- data/packages/gcutil-1.7.1/lib/google_api_python_client/oauth2client/file.py +107 -0
- data/packages/gcutil-1.7.1/lib/google_api_python_client/oauth2client/locked_file.py +343 -0
- data/packages/gcutil-1.7.1/lib/google_api_python_client/oauth2client/multistore_file.py +379 -0
- data/packages/gcutil-1.7.1/lib/google_api_python_client/oauth2client/tools.py +174 -0
- data/packages/gcutil-1.7.1/lib/google_api_python_client/uritemplate/__init__.py +147 -0
- data/packages/gcutil-1.7.1/lib/google_apputils/LICENSE +202 -0
- data/packages/gcutil-1.7.1/lib/google_apputils/google/__init__.py +3 -0
- data/packages/gcutil-1.7.1/lib/google_apputils/google/apputils/__init__.py +3 -0
- data/packages/gcutil-1.7.1/lib/google_apputils/google/apputils/app.py +356 -0
- data/packages/gcutil-1.7.1/lib/google_apputils/google/apputils/appcommands.py +783 -0
- data/packages/gcutil-1.7.1/lib/google_apputils/google/apputils/basetest.py +1260 -0
- data/packages/gcutil-1.7.1/lib/google_apputils/google/apputils/datelib.py +421 -0
- data/packages/gcutil-1.7.1/lib/google_apputils/google/apputils/debug.py +60 -0
- data/packages/gcutil-1.7.1/lib/google_apputils/google/apputils/file_util.py +181 -0
- data/packages/gcutil-1.7.1/lib/google_apputils/google/apputils/resources.py +67 -0
- data/packages/gcutil-1.7.1/lib/google_apputils/google/apputils/run_script_module.py +217 -0
- data/packages/gcutil-1.7.1/lib/google_apputils/google/apputils/setup_command.py +159 -0
- data/packages/gcutil-1.7.1/lib/google_apputils/google/apputils/shellutil.py +49 -0
- data/packages/gcutil-1.7.1/lib/google_apputils/google/apputils/stopwatch.py +204 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/__init__.py +0 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/auth_helper.py +140 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/auth_helper_test.py +149 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/auto_auth.py +130 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/auto_auth_test.py +75 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/basic_cmds.py +128 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/basic_cmds_test.py +111 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/command_base.py +1808 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/command_base_test.py +1651 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/compute/v1beta13.json +2851 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/compute/v1beta14.json +3361 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/disk_cmds.py +342 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/disk_cmds_test.py +474 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/firewall_cmds.py +344 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/firewall_cmds_test.py +231 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/flags_cache.py +274 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/gcutil +89 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/gcutil_logging.py +69 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/image_cmds.py +262 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/image_cmds_test.py +172 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/instance_cmds.py +1506 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/instance_cmds_test.py +1904 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/kernel_cmds.py +91 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/kernel_cmds_test.py +56 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/machine_type_cmds.py +106 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/machine_type_cmds_test.py +59 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/metadata.py +96 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/metadata_lib.py +357 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/metadata_test.py +84 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/mock_api.py +420 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/mock_metadata.py +58 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/move_cmds.py +824 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/move_cmds_test.py +307 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/network_cmds.py +178 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/network_cmds_test.py +133 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/operation_cmds.py +181 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/operation_cmds_test.py +196 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/path_initializer.py +38 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/project_cmds.py +173 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/project_cmds_test.py +111 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/scopes.py +61 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/scopes_test.py +50 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/snapshot_cmds.py +276 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/snapshot_cmds_test.py +260 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/ssh_keys.py +266 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/ssh_keys_test.py +128 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/table_formatter.py +563 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/thread_pool.py +188 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/thread_pool_test.py +88 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/utils.py +208 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/utils_test.py +193 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/version.py +17 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/version_checker.py +246 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/version_checker_test.py +271 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/zone_cmds.py +151 -0
- data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/zone_cmds_test.py +60 -0
- data/packages/gcutil-1.7.1/lib/httplib2/LICENSE +21 -0
- data/packages/gcutil-1.7.1/lib/httplib2/httplib2/__init__.py +1630 -0
- data/packages/gcutil-1.7.1/lib/httplib2/httplib2/cacerts.txt +714 -0
- data/packages/gcutil-1.7.1/lib/httplib2/httplib2/iri2uri.py +110 -0
- data/packages/gcutil-1.7.1/lib/httplib2/httplib2/socks.py +438 -0
- data/packages/gcutil-1.7.1/lib/iso8601/LICENSE +20 -0
- data/packages/gcutil-1.7.1/lib/iso8601/iso8601/__init__.py +1 -0
- data/packages/gcutil-1.7.1/lib/iso8601/iso8601/iso8601.py +102 -0
- data/packages/gcutil-1.7.1/lib/iso8601/iso8601/test_iso8601.py +111 -0
- data/packages/gcutil-1.7.1/lib/python_gflags/AUTHORS +2 -0
- data/packages/gcutil-1.7.1/lib/python_gflags/LICENSE +28 -0
- data/packages/gcutil-1.7.1/lib/python_gflags/gflags.py +2862 -0
- data/packages/gcutil-1.7.1/lib/python_gflags/gflags2man.py +544 -0
- data/packages/gcutil-1.7.1/lib/python_gflags/gflags_validators.py +187 -0
- metadata +118 -5
- 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
|