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,172 @@
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 machine image commands."""
18
+
19
+
20
+
21
+ import path_initializer
22
+ path_initializer.InitializeSysPath()
23
+
24
+ import copy
25
+
26
+ import gflags as flags
27
+ import unittest
28
+
29
+ from gcutil import command_base
30
+ from gcutil import image_cmds
31
+ from gcutil import mock_api
32
+
33
+ FLAGS = flags.FLAGS
34
+
35
+
36
+ class ImageCmdsTest(unittest.TestCase):
37
+
38
+ def _doTestAddImageGeneratesCorrectRequest(self, service_version,
39
+ requested_source,
40
+ expected_source):
41
+ flag_values = copy.deepcopy(FLAGS)
42
+
43
+ command = image_cmds.AddImage('addimage', flag_values)
44
+
45
+ expected_project = 'test_project'
46
+ expected_image = 'test_image'
47
+ expected_description = 'test image'
48
+ submitted_kernel = 'projects/test_project/kernels/test_kernel'
49
+ expected_type = 'RAW'
50
+ flag_values.project = expected_project
51
+ flag_values.description = expected_description
52
+ flag_values.preferred_kernel = submitted_kernel
53
+ flag_values.service_version = service_version
54
+
55
+ command.SetFlags(flag_values)
56
+ command.SetApi(mock_api.MockApi())
57
+
58
+ expected_kernel = command.NormalizeGlobalResourceName(expected_project,
59
+ 'kernels',
60
+ submitted_kernel)
61
+
62
+ result = command.Handle(expected_image, requested_source)
63
+
64
+ self.assertEqual(result['project'], expected_project)
65
+ self.assertEqual(result['body']['name'], expected_image)
66
+ self.assertEqual(result['body']['description'], expected_description)
67
+
68
+ self.assertEqual(result['body']['preferredKernel'], expected_kernel)
69
+ self.assertEqual(result['body']['sourceType'], expected_type)
70
+ self.assertEqual(result['body']['rawDisk']['source'], expected_source)
71
+
72
+ def testAddImageGeneratesCorrectRequest(self):
73
+ for version in command_base.SUPPORTED_VERSIONS:
74
+ self._doTestAddImageGeneratesCorrectRequest(
75
+ version, 'http://test.source', 'http://test.source')
76
+ self._doTestAddImageGeneratesCorrectRequest(
77
+ version, 'gs://test_bucket/source',
78
+ 'http://storage.googleapis.com/test_bucket/source')
79
+
80
+ def testGetImageGeneratesCorrectRequest(self):
81
+ flag_values = copy.deepcopy(FLAGS)
82
+
83
+ command = image_cmds.GetImage('getimage', flag_values)
84
+
85
+ expected_project = 'test_project'
86
+ expected_image = 'test_image'
87
+ flag_values.project = expected_project
88
+
89
+ command.SetFlags(flag_values)
90
+ command.SetApi(mock_api.MockApi())
91
+
92
+ result = command.Handle(expected_image)
93
+
94
+ self.assertEqual(result['project'], expected_project)
95
+ self.assertEqual(result['image'], expected_image)
96
+
97
+ def testDeleteImageGeneratesCorrectRequest(self):
98
+ flag_values = copy.deepcopy(FLAGS)
99
+
100
+ command = image_cmds.DeleteImage('deleteimage', flag_values)
101
+
102
+ expected_project = 'test_project'
103
+ expected_image = 'test_image'
104
+ flag_values.project = expected_project
105
+
106
+ command.SetFlags(flag_values)
107
+ command.SetApi(mock_api.MockApi())
108
+ command._credential = mock_api.MockCredential()
109
+
110
+ results, exceptions = command.Handle(expected_image)
111
+ self.assertEqual(exceptions, [])
112
+ self.assertEqual(len(results['items']), 1)
113
+ result = results['items'][0]
114
+
115
+ self.assertEqual(result['project'], expected_project)
116
+ self.assertEqual(result['image'], expected_image)
117
+
118
+ def testDeleteMultipleImages(self):
119
+ flag_values = copy.deepcopy(FLAGS)
120
+ command = image_cmds.DeleteImage('deleteimage', flag_values)
121
+
122
+ expected_project = 'test_project'
123
+ expected_images = ['test-image-%02d' % x for x in xrange(100)]
124
+ flag_values.project = expected_project
125
+
126
+ command.SetFlags(flag_values)
127
+ command.SetApi(mock_api.MockApi())
128
+ command._credential = mock_api.MockCredential()
129
+
130
+ results, exceptions = command.Handle(*expected_images)
131
+ self.assertEqual(exceptions, [])
132
+ results = results['items']
133
+ self.assertEqual(len(results), len(expected_images))
134
+
135
+ for expected_image, result in zip(expected_images, results):
136
+ self.assertEqual(result['project'], expected_project)
137
+ self.assertEqual(result['image'], expected_image)
138
+
139
+ def testDeprecate(self):
140
+ flag_values = copy.deepcopy(FLAGS)
141
+
142
+ command = image_cmds.Deprecate('deprecateimage', flag_values)
143
+
144
+ expected_project = 'test_project'
145
+ expected_image = 'test_image'
146
+ expected_state = 'DEPRECATED'
147
+ expected_replacement = 'replacement_image'
148
+ expected_obsolete_timestamp = '1970-01-01T00:00:00Z'
149
+ expected_deleted_timestamp = '1980-01-01T00:00:00.000Z'
150
+ flag_values.project = expected_project
151
+ flag_values.state = expected_state
152
+ flag_values.replacement = expected_replacement
153
+ flag_values.obsolete_on = expected_obsolete_timestamp
154
+ flag_values.deleted_on = expected_deleted_timestamp
155
+
156
+ command.SetFlags(flag_values)
157
+ command.SetApi(mock_api.MockApi())
158
+
159
+ result = command.Handle(expected_image)
160
+
161
+ self.assertEqual(result['project'], expected_project)
162
+ self.assertEqual(result['image'], expected_image)
163
+ self.assertEqual(result['body']['state'], expected_state)
164
+ self.assertEqual(result['body']['replacement'],
165
+ command.NormalizeGlobalResourceName(
166
+ expected_project, 'images', expected_replacement))
167
+ self.assertEqual(result['body']['obsolete'], expected_obsolete_timestamp)
168
+ self.assertEqual(result['body']['deleted'], expected_deleted_timestamp)
169
+
170
+
171
+ if __name__ == '__main__':
172
+ unittest.main()
@@ -0,0 +1,1506 @@
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
+ """Commands for interacting with Google Compute Engine VM instances."""
16
+
17
+
18
+
19
+
20
+ import logging
21
+ import os
22
+ import time
23
+
24
+ from apiclient import errors
25
+
26
+ from google.apputils import app
27
+ from google.apputils import appcommands
28
+ import gflags as flags
29
+
30
+ from gcutil import command_base
31
+ from gcutil import gcutil_logging
32
+ from gcutil import metadata
33
+ from gcutil import scopes
34
+ from gcutil import ssh_keys
35
+
36
+
37
+
38
+ FLAGS = flags.FLAGS
39
+ LOGGER = gcutil_logging.LOGGER
40
+
41
+
42
+ class InstanceCommand(command_base.GoogleComputeCommand):
43
+ """Base command for working with the instances collection."""
44
+
45
+ default_sort_field = 'name'
46
+ summary_fields = (('name', 'name'),
47
+ ('machine-type', 'machineType'),
48
+ ('image', 'image'),
49
+ ('network', 'networkInterfaces.network'),
50
+ ('network-ip', 'networkInterfaces.networkIP'),
51
+ ('external-ip', 'networkInterfaces.accessConfigs.natIP'),
52
+ ('disks', 'disks.source'),
53
+ ('zone', 'zone'),
54
+ ('status', 'status'),
55
+ ('status-message', 'statusMessage'))
56
+
57
+ # The remaining complex fields are filled in via CustomizePrintResult
58
+ detail_fields = (('name', 'name'),
59
+ ('description', 'description'),
60
+ ('creation-time', 'creationTimestamp'),
61
+ ('machine', 'machineType'),
62
+ ('image', 'image'),
63
+ ('zone', 'zone'),
64
+ ('tags-fingerprint', 'tags.fingerprint'),
65
+ ('metadata-fingerprint', 'metadata.fingerprint'),
66
+ ('status', 'status'),
67
+ ('status-message', 'statusMessage'))
68
+
69
+ # A map from legal values for the disk "mode" option to the
70
+ # corresponding API value. Keys in this map should be lowercase, as
71
+ # we convert user provided values to lowercase prior to performing a
72
+ # look-up.
73
+ disk_modes = {
74
+ 'read_only': 'READ_ONLY',
75
+ 'ro': 'READ_ONLY',
76
+ 'read_write': 'READ_WRITE',
77
+ 'rw': 'READ_WRITE'}
78
+
79
+ resource_collection_name = 'instances'
80
+
81
+ # The default network interface name assigned by the service.
82
+ DEFAULT_NETWORK_INTERFACE_NAME = 'nic0'
83
+
84
+ # The default access config name
85
+ DEFAULT_ACCESS_CONFIG_NAME = 'External NAT'
86
+
87
+ # Currently, only access config type 'ONE_TO_ONE_NAT' is supported.
88
+ ONE_TO_ONE_NAT_ACCESS_CONFIG_TYPE = 'ONE_TO_ONE_NAT'
89
+
90
+ # Let the server select an ephemeral IP address.
91
+ EPHEMERAL_ACCESS_CONFIG_NAT_IP = 'ephemeral'
92
+
93
+ def __init__(self, name, flag_values):
94
+ super(InstanceCommand, self).__init__(name, flag_values)
95
+
96
+ flags.DEFINE_string('zone',
97
+ None,
98
+ 'The zone for this request.',
99
+ flag_values=flag_values)
100
+
101
+ def SetApi(self, api):
102
+ """Set the Google Compute Engine API for the command.
103
+
104
+ Args:
105
+ api: The Google Compute Engine API used by this command.
106
+
107
+ Returns:
108
+ None.
109
+
110
+ """
111
+ self._projects_api = api.projects()
112
+ self._instances_api = api.instances()
113
+ self._images_api = api.images()
114
+ self._kernels_api = api.kernels()
115
+ self._disks_api = api.disks()
116
+ self._machine_types_api = api.machineTypes()
117
+ self._zones_api = api.zones()
118
+
119
+ def CustomizePrintResult(self, result, table):
120
+ """Customized result printing for this type.
121
+
122
+ Args:
123
+ result: json dictionary returned by the server
124
+ table: the pretty printing table to be customized
125
+
126
+ Returns:
127
+ None.
128
+
129
+ """
130
+ # Add the disks
131
+ for disk in result.get('disks', []):
132
+ table.AddRow(('', ''))
133
+ table.AddRow(('disk', disk['index']))
134
+ table.AddRow((' type', disk['type']))
135
+ if 'mode' in disk:
136
+ table.AddRow((' mode', disk['mode']))
137
+ if 'deviceName' in disk:
138
+ table.AddRow((' deviceName', disk['deviceName']))
139
+ if 'source' in disk:
140
+ table.AddRow((' source', disk['source']))
141
+ if 'boot' in disk:
142
+ table.AddRow((' boot', disk['boot']))
143
+ if 'deleteOnTerminate' in disk:
144
+ table.AddRow((' delete on terminate', disk['deleteOnTerminate']))
145
+
146
+ # Add the networks
147
+ for network in result.get('networkInterfaces', []):
148
+ table.AddRow(('', ''))
149
+ table.AddRow(('network-interface', ''))
150
+ table.AddRow((' network',
151
+ self._PresentElement(network.get('network', ''))))
152
+ table.AddRow((' ip', network.get('networkIP', '')))
153
+ for config in network.get('accessConfigs', []):
154
+ table.AddRow((' access-configuration', config.get('name', '')))
155
+ table.AddRow((' type', config.get('type', '')))
156
+ table.AddRow((' external-ip', config.get('natIP', '')))
157
+
158
+ # Add the service accounts
159
+ for service_account in result.get('serviceAccounts', []):
160
+ table.AddRow(('', ''))
161
+ table.AddRow(('service-account', service_account.get('email', '')))
162
+ table.AddRow((' scopes', service_account.get('scopes', '')))
163
+
164
+ # Add metadata
165
+
166
+ if result.get('metadata', []):
167
+ table.AddRow(('', ''))
168
+ table.AddRow(('metadata', ''))
169
+ table.AddRow(('fingerprint', result.get('metadata', {})
170
+ .get('fingerprint', '')))
171
+ metadata_container = result.get('metadata', {}).get('items', [])
172
+ for i in metadata_container:
173
+ table.AddRow((' %s' % i.get('key', ''),
174
+ self._PresentElement(i.get('value', ''))))
175
+
176
+ # Add tags
177
+
178
+ if result.get('tags', []):
179
+ table.AddRow(('', ''))
180
+ table.AddRow(('tags', ''))
181
+ table.AddRow(('fingerprint', result.get('tags', {})
182
+ .get('fingerprint', '')))
183
+ tags_container = result.get('tags', {}).get('items', [])
184
+ for i in tags_container:
185
+ table.AddRow((' ',
186
+ self._PresentElement(i)))
187
+
188
+ def _ExtractExternalIpFromInstanceRecord(self, instance_record):
189
+ """Extract the external IP(s) from an instance record.
190
+
191
+ Args:
192
+ instance_record: An instance as returned by the Google Compute Engine API.
193
+
194
+ Returns:
195
+ A list of internet IP addresses associated with this VM.
196
+ """
197
+ external_ips = set()
198
+
199
+ for network_interface in instance_record.get('networkInterfaces', []):
200
+ for access_config in network_interface.get('accessConfigs', []):
201
+ # At the moment, we only know how to translate 1-to-1 NAT
202
+ if (access_config.get('type') == self.ONE_TO_ONE_NAT_ACCESS_CONFIG_TYPE
203
+ and 'natIP' in access_config):
204
+ external_ips.add(access_config['natIP'])
205
+
206
+ return list(external_ips)
207
+
208
+ def _AddAuthorizedUserKeyToProject(self, authorized_user_key):
209
+ """Update the project to include the specified user/key pair.
210
+
211
+ Args:
212
+ authorized_user_key: A dictionary of a user/key pair for the user.
213
+
214
+ Returns:
215
+ True iff the ssh key was added to the project.
216
+
217
+ Raises:
218
+ command_base.CommandError: If the metadata update fails.
219
+ """
220
+ project = self._projects_api.get(project=self._project).execute()
221
+ common_instance_metadata = project.get('commonInstanceMetadata', {})
222
+
223
+ project_metadata = common_instance_metadata.get(
224
+ 'items', [])
225
+ project_ssh_keys = ssh_keys.SshKeys.GetAuthorizedUserKeysFromMetadata(
226
+ project_metadata)
227
+ if authorized_user_key in project_ssh_keys:
228
+ return False
229
+ else:
230
+ project_ssh_keys.append(authorized_user_key)
231
+ ssh_keys.SshKeys.SetAuthorizedUserKeysInMetadata(
232
+ project_metadata, project_ssh_keys)
233
+
234
+ try:
235
+ request = self._projects_api.setCommonInstanceMetadata(
236
+ project=self._project,
237
+ body={'kind': self._GetResourceApiKind('metadata'),
238
+ 'items': project_metadata})
239
+ request.execute()
240
+ except errors.HttpError:
241
+ # A failure to add the ssh key probably means that the project metadata
242
+ # has exceeded the max size. The user needs to either manually
243
+ # clean up their project metadata, or set the ssh keys manually for this
244
+ # instance. Either way, trigger a usage error to let them know.
245
+ raise command_base.CommandError(
246
+ 'Unable to add the local ssh key to the project. Either manually '
247
+ 'remove some entries from the commonInstanceMetadata field of the '
248
+ 'project, or explicitly set the authorized keys for this instance.')
249
+ return True
250
+
251
+ def _PrepareRequestArgs(self, instance_name, **other_args):
252
+ """Gets the dictionary of API method keyword arguments.
253
+
254
+ Args:
255
+ instance_name: The name of the instance.
256
+ **other_args: Keyword arguments that should be included in the request.
257
+
258
+ Returns:
259
+ Dictionary of keyword arguments that should be passed in the API call,
260
+ includes all keyword arguments passed in 'other_args' plus
261
+ common keys such as the name of the resource and the project.
262
+ """
263
+
264
+ kwargs = {
265
+ 'project': self._project,
266
+ 'instance': self.DenormalizeResourceName(instance_name)
267
+ }
268
+ if self._IsUsingAtLeastApiVersion('v1beta14'):
269
+ if not self._flags.zone:
270
+ self._flags.zone = self.GetZoneForResource(self._instances_api,
271
+ instance_name)
272
+ kwargs['zone'] = self._flags.zone
273
+
274
+ for key, value in other_args.items():
275
+ kwargs[key] = value
276
+ return kwargs
277
+
278
+ def _AddComputeKeyToProject(self):
279
+ """Update the current project to include the user's public ssh key.
280
+
281
+ Returns:
282
+ True iff the ssh key was added to the project.
283
+ """
284
+ compute_key = ssh_keys.SshKeys.GetPublicKey()
285
+ return self._AddAuthorizedUserKeyToProject(compute_key)
286
+
287
+ def _BuildAttachedDisk(self, disk_arg):
288
+ """Converts a disk argument into an AttachedDisk object."""
289
+ # Start with the assumption that the argument only specifies the
290
+ # name of the disk resource.
291
+ disk_name = disk_arg
292
+ device_name = disk_arg
293
+ mode = 'READ_WRITE'
294
+ boot = False
295
+
296
+ disk_parts = disk_arg.split(',')
297
+ if len(disk_parts) > 1:
298
+ # The argument includes new-style decorators. The first part is
299
+ # the disk resource name. The other parts are optional key/value
300
+ # pairs.
301
+ disk_name = disk_parts[0]
302
+ device_name = disk_parts[0]
303
+ for option in disk_parts[1:]:
304
+ if option == 'boot':
305
+ if self._IsUsingAtLeastApiVersion('v1beta14'):
306
+ boot = True
307
+ continue
308
+ else:
309
+ raise ValueError('boot flag is not supported for this API version')
310
+ if not '=' in option:
311
+ raise ValueError('Invalid disk option: %s' % option)
312
+ key, value = option.split('=', 2)
313
+ if key == 'deviceName':
314
+ device_name = value
315
+ elif key == 'mode':
316
+ mode = self.disk_modes.get(value.lower())
317
+ if not mode:
318
+ raise ValueError('Invalid disk mode: %s' % value)
319
+ else:
320
+ raise ValueError('Invalid disk option: %s' % key)
321
+ else:
322
+ # The user didn't provide any options using the newer key/value
323
+ # syntax, so check to see if they have used the old syntax where
324
+ # the device name is delimited by a colon.
325
+ disk_parts = disk_arg.split(':')
326
+ if len(disk_parts) > 1:
327
+ disk_name = disk_parts[0]
328
+ device_name = disk_parts[1]
329
+ LOGGER.info(
330
+ 'Please use new disk device naming syntax: --disk=%s,deviceName=%s',
331
+ disk_name,
332
+ device_name)
333
+
334
+ disk_url = self.NormalizePerZoneResourceName(self._project,
335
+ self._flags.zone,
336
+ 'disks',
337
+ disk_name)
338
+
339
+ disk = {
340
+ 'type': 'PERSISTENT',
341
+ 'source': disk_url,
342
+ 'mode': mode,
343
+ 'deviceName': device_name}
344
+ if self._IsUsingAtLeastApiVersion('v1beta14'):
345
+ disk['boot'] = boot
346
+ return disk
347
+
348
+
349
+ class AddInstance(InstanceCommand):
350
+ """Create new VM instances.
351
+
352
+ More than one instance name can be specified. Multiple instances will be
353
+ created in parallel.
354
+ """
355
+
356
+ positional_args = '<instance-name-1> ... <instance-name-n>'
357
+ status_field = 'status'
358
+ _TERMINAL_STATUS = ['RUNNING', 'TERMINATED']
359
+
360
+ def __init__(self, name, flag_values):
361
+ super(AddInstance, self).__init__(name, flag_values)
362
+
363
+ flags.DEFINE_string('description',
364
+ '',
365
+ 'Instance description',
366
+ flag_values=flag_values)
367
+ flags.DEFINE_string('image',
368
+ None,
369
+ 'Image name. To get a list of images built by Google, '
370
+ 'run \'gcutil listimages --project=projects/google\'. '
371
+ 'To get a list of images you have built, run \'gcutil '
372
+ 'listimages\'.',
373
+ flag_values=flag_values)
374
+ flags.DEFINE_string('kernel',
375
+ None,
376
+ 'Kernel name. To get a list of kernels built by '
377
+ 'Google, run \'gcutil listkernels --project=google\'. ',
378
+ flag_values=flag_values)
379
+ flags.DEFINE_boolean('persistent_boot_disk',
380
+ None,
381
+ 'Make boot disk persistent. Copy contents of the '
382
+ 'image onto a new disk named "boot-{instanceName}" '
383
+ 'and use it for booting. The preferred kernel for '
384
+ 'the image will be used to boot, but it may be '
385
+ 'overridden by passing --kernel.',
386
+ flag_values=flag_values)
387
+ flags.DEFINE_string('machine_type',
388
+ None,
389
+ 'Machine type name. To get a list of available machine '
390
+ 'types, run \'gcutil listmachinetypes\'.',
391
+ flag_values=flag_values)
392
+ flags.DEFINE_string('network',
393
+ 'default',
394
+ 'The network to which to attach the instance.',
395
+ flag_values=flag_values)
396
+ flags.DEFINE_string('internal_ip_address',
397
+ '',
398
+ 'The internal (within the specified network) IP '
399
+ 'address for the instance; if not set the instance '
400
+ 'will be assigned an appropriate address.',
401
+ flag_values=flag_values)
402
+ flags.DEFINE_string('external_ip_address',
403
+ self.EPHEMERAL_ACCESS_CONFIG_NAT_IP,
404
+ 'The external NAT IP of the new instance. The default '
405
+ 'value "ephemeral" indicates the service should choose '
406
+ 'an available ephemeral IP. The value "none" (or an '
407
+ 'empty string) indicates no external IP will be '
408
+ 'assigned to the new instance. If an explicit IP is '
409
+ 'given, that IP must be reserved by the project and '
410
+ 'not yet assigned to another instance.',
411
+ flag_values=flag_values)
412
+ flags.DEFINE_multistring('disk',
413
+ [],
414
+ 'The name of a disk to be attached to the '
415
+ 'instance. The name may be followed by a '
416
+ 'comma-separated list of name=value pairs '
417
+ 'specifying options. Legal option names are '
418
+ '\'deviceName\', to specify the disk\'s device '
419
+ 'name, and \'mode\', to indicate whether the disk '
420
+ 'should be attached READ_WRITE (the default) or '
421
+ 'READ_ONLY. You may also use the \'boot\' '
422
+ 'flag to designate the disk as a boot device',
423
+ flag_values=flag_values)
424
+ flags.DEFINE_boolean('use_compute_key',
425
+ False,
426
+ 'Whether or not to include the default '
427
+ 'Google Compute Engine ssh key as one of the '
428
+ 'authorized ssh keys for the created instance. This '
429
+ 'has the side effect of disabling project-wide ssh '
430
+ 'key management for the instance.',
431
+ flag_values=flag_values)
432
+ flags.DEFINE_boolean('add_compute_key_to_project',
433
+ None,
434
+ 'Whether or not to add the default Google Compute '
435
+ 'Engine ssh key as one of the authorized ssh keys '
436
+ 'for the project. If the default key has already '
437
+ 'been added to the project, then this will have no '
438
+ 'effect. The default behavior is to add the key to '
439
+ 'the project if no instance-specific keys are '
440
+ 'defined.',
441
+ flag_values=flag_values)
442
+ flags.DEFINE_list('authorized_ssh_keys',
443
+ [],
444
+ 'Fix the list of user/key-file pairs to the specified '
445
+ 'entries, disabling project-wide key management for this '
446
+ 'instance. These are specified as a comma separated list '
447
+ 'of colon separated entries: '
448
+ 'user1:keyfile1,user2:keyfile2,...',
449
+ flag_values=flag_values)
450
+ flags.DEFINE_string('service_account',
451
+ 'default',
452
+ 'The service account whose credentials are to be made'
453
+ ' available for this instance.',
454
+ flag_values=flag_values)
455
+ flags.DEFINE_list('service_account_scopes',
456
+ [],
457
+ 'The scopes of credentials of the above service'
458
+ ' account that are to be made available for this'
459
+ ' instance (comma separated). There are also a set of '
460
+ 'scope aliases supported: %s'
461
+ % ', '.join(sorted(scopes.SCOPE_ALIASES.keys())),
462
+ flag_values=flag_values)
463
+ flags.DEFINE_boolean('wait_until_running',
464
+ False,
465
+ 'Whether the program should wait until the instance is'
466
+ ' in running state.',
467
+ flag_values=flag_values)
468
+ flags.DEFINE_list('tags',
469
+ [],
470
+ 'A set of tags applied to this instance. Used for '
471
+ 'filtering and to configure network firewall rules '
472
+ '(comma separated).',
473
+ flag_values=flag_values)
474
+
475
+ self._metadata_flags_processor = metadata.MetadataFlagsProcessor(
476
+ flag_values)
477
+
478
+ def Handle(self, *instance_names):
479
+ """Add the specified instance.
480
+
481
+ Args:
482
+ *instance_names: A list of instance names to add.
483
+
484
+ Returns:
485
+ A tuple of (result, exceptions)
486
+ """
487
+ if not instance_names:
488
+ raise app.UsageError('You must specify at least one instance name')
489
+
490
+ if len(instance_names) > 1 and self._flags.disk:
491
+ raise command_base.CommandError(
492
+ 'Specifying a disk when starting multiple instances is not '
493
+ 'currently supported')
494
+
495
+ if max([len(i) for i in instance_names]) > 32:
496
+ LOGGER.warn('Hostnames longer than 32 characters have known issues with '
497
+ 'some linux distributions.')
498
+
499
+ self._flags.zone = self._GetZone(self._flags.zone or
500
+ self._FindDefaultZone(self._flags.disk))
501
+ if not self._flags.machine_type:
502
+ self._flags.machine_type = self._PromptForMachineType()['name']
503
+
504
+ # Processes the disks, so we can check for the presence of a boot
505
+ # disk before prompting for image or kernel.
506
+ disks = [self._BuildAttachedDisk(disk) for disk in self._flags.disk]
507
+
508
+ if (not self._flags.image and
509
+ self._IsUsingAtLeastApiVersion('v1beta14') and
510
+ (not self._HasBootDisk(disks) or self._flags.persistent_boot_disk)):
511
+ self._flags.image = self._PromptForImage()['selfLink']
512
+
513
+ if not self._flags.kernel and self._HasBootDisk(disks):
514
+ # Have boot disk but no kernel, prompt for a kernel.
515
+ self._flags.kernel = self._PromptForKernel()['selfLink']
516
+
517
+ instance_metadata = self._metadata_flags_processor.GatherMetadata()
518
+ if self._flags.authorized_ssh_keys or self._flags.use_compute_key:
519
+ instance_metadata = self._AddSshKeysToMetadata(instance_metadata)
520
+
521
+ # Map of instance_name => boot_disk.
522
+ boot_disks = {}
523
+ if self._flags.persistent_boot_disk:
524
+ if not self._IsUsingAtLeastApiVersion('v1beta14'):
525
+ raise app.UsageError(
526
+ 'Booting from persistent disk is only supported in '
527
+ 'v1beta14 and above.')
528
+
529
+ # Persistent boot device request. We need to create a new disk for each VM
530
+ # and populate it with contents of the specified image.
531
+
532
+ # Read the preferred kernel from the image unless overridden by the user.
533
+ if not self._flags.kernel:
534
+ normalized_image_name = self.NormalizeGlobalResourceName(
535
+ self._project, 'images', self._flags.image)
536
+ image_name_parts = normalized_image_name.split('/')
537
+
538
+ # Read the actual image, but first verify that the user gave us valid
539
+ # image URL.
540
+ if (image_name_parts[-2] != 'images' or
541
+ image_name_parts[-3] != 'global' or
542
+ image_name_parts[-5] != 'projects'):
543
+ raise app.UsageError('Invalid image URL: %s' %
544
+ normalized_image_name)
545
+
546
+ image_resource = self._images_api.get(
547
+ project=image_name_parts[-4],
548
+ image=image_name_parts[-1]).execute()
549
+
550
+ self._flags.kernel = image_resource['preferredKernel']
551
+
552
+ disk_creation_requests = []
553
+ for instance_name in instance_names:
554
+ boot_disk_name = 'boot-%s' % (instance_name)
555
+ boot_disks[instance_name] = self._BuildAttachedDisk(
556
+ '%s,boot' % (boot_disk_name))
557
+ LOGGER.info('Preparing boot disk [%s] for instance [%s]'
558
+ ' from disk image [%s].',
559
+ boot_disk_name, instance_name,
560
+ self._flags.image)
561
+ disk_creation_requests.append(
562
+ self._CreateDiskFromImageRequest(boot_disk_name))
563
+
564
+ self._flags.image = None
565
+
566
+ (disk_results, disk_exceptions) = self.ExecuteRequests(
567
+ disk_creation_requests, collection_name='disks')
568
+ if disk_exceptions:
569
+ return (self.MakeListResult(disk_results, 'operationList'),
570
+ disk_exceptions)
571
+
572
+ if self._flags.add_compute_key_to_project or (
573
+ self._flags.add_compute_key_to_project is None and
574
+ not 'sshKeys' in [entry.get('key', '') for entry in instance_metadata]):
575
+ try:
576
+ self._AddComputeKeyToProject()
577
+ except ssh_keys.UserSetupError as e:
578
+ LOGGER.warn('Could not generate compute ssh key: %s', e)
579
+
580
+ self._ValidateFlags()
581
+
582
+ requests = []
583
+ for instance_name in instance_names:
584
+ instance_disks = disks
585
+ if instance_name in boot_disks:
586
+ instance_disks = [boot_disks[instance_name]] + disks
587
+ requests.append(self._BuildRequestWithMetadata(
588
+ instance_name, instance_metadata, instance_disks))
589
+
590
+ (results, exceptions) = self.ExecuteRequests(requests)
591
+
592
+ if self._flags.wait_until_running:
593
+ instances_to_wait = results
594
+ results = []
595
+ for result in instances_to_wait:
596
+ if self.IsResultAnOperation(result):
597
+ results.append(result)
598
+ else:
599
+ instance_name = result['name']
600
+ kwargs = self._PrepareRequestArgs(instance_name)
601
+ get_request = self._instances_api.get(**kwargs)
602
+ instance_result = get_request.execute()
603
+ instance_result = self._WaitUntilInstanceIsRunning(
604
+ instance_result, kwargs)
605
+ results.append(instance_result)
606
+
607
+ if self._flags.synchronous_mode:
608
+ return (self.MakeListResult(results, 'instanceList'), exceptions)
609
+ else:
610
+ return (self.MakeListResult(results, 'operationList'), exceptions)
611
+
612
+ def _WaitUntilInstanceIsRunning(self, result, kwargs):
613
+ """Waits for the instance to start.
614
+
615
+ Periodically polls the server for current instance status. Exits if the
616
+ status of the instance is RUNNING or TERMINATED or the maximum waiting
617
+ timeout has been reached. In both cases returns the last known instance
618
+ details.
619
+
620
+ Args:
621
+ result: the current state of the instance.
622
+ kwargs: keyword arguments to _instances_api.get()
623
+
624
+ Returns:
625
+ Json containing full instance information.
626
+ """
627
+ current_status = result[self.status_field]
628
+ start_time = time.time()
629
+ instance_name = kwargs['instance']
630
+ LOGGER.info('Ensuring %s is running. Will wait to start for: %d seconds.',
631
+ instance_name, self._flags.max_wait_time)
632
+ while (time.time() - start_time < self._flags.max_wait_time and
633
+ current_status not in self._TERMINAL_STATUS):
634
+ LOGGER.info(
635
+ 'Waiting for instance \'%s\' to start. '
636
+ 'Current status: %s. Sleeping for %ss.',
637
+ instance_name,
638
+ current_status, self._flags.sleep_between_polls)
639
+ time.sleep(self._flags.sleep_between_polls)
640
+ result = self._instances_api.get(**kwargs).execute()
641
+ current_status = result[self.status_field]
642
+ if current_status not in self._TERMINAL_STATUS:
643
+ LOGGER.warn('Timeout reached. Instance %s has not yet started.',
644
+ instance_name)
645
+ return result
646
+
647
+ def _FindDefaultZone(self, disks):
648
+ """Given the persistent disks for an instance, find a default zone.
649
+
650
+ Args:
651
+ disks: The list of persistent disks to be used by the instance.
652
+
653
+ Returns:
654
+ The name of a zone if a clear default can be determined
655
+ from the persistent disks, otherwise None.
656
+ """
657
+ for disk in disks:
658
+ # Remove any options from the disk name. We need to strip using
659
+ # both ',' and ':' to handle the new and old methods for
660
+ # providing disk options.
661
+ if ',' in disk:
662
+ disk = disk.split(',')[0]
663
+ elif ':' in disk:
664
+ disk = disk.split(':')[0]
665
+ disk_name = self.DenormalizeResourceName(disk)
666
+
667
+ if self._IsUsingAtLeastApiVersion('v1beta14'):
668
+ return self.GetZoneForResource(self._disks_api, disk_name,
669
+ fail_if_not_found=False)
670
+
671
+ get_request = self._disks_api.get(
672
+ project=self._project, disk=disk_name)
673
+ return get_request.execute()['zone']
674
+
675
+ def _AddSshKeysToMetadata(self, instance_metadata):
676
+ instance_ssh_keys = ssh_keys.SshKeys.GetAuthorizedUserKeys(
677
+ use_compute_key=self._flags.use_compute_key,
678
+ authorized_ssh_keys=self._flags.authorized_ssh_keys)
679
+ if instance_ssh_keys:
680
+ new_value = ['%(user)s:%(key)s' % user_key
681
+ for user_key in instance_ssh_keys]
682
+ # Have the new value extend the old value
683
+ old_values = [entry['value'] for entry in instance_metadata
684
+ if entry['key'] == 'sshKeys']
685
+ all_values = '\n'.join(old_values + new_value)
686
+ instance_metadata = [entry for entry in instance_metadata
687
+ if entry['key'] != 'sshKeys']
688
+ instance_metadata.append({'key': 'sshKeys', 'value': all_values})
689
+ return instance_metadata
690
+
691
+ def _HasBootDisk(self, disks):
692
+ """Determines if any of the disks in a list is a boot disk."""
693
+ for disk in disks:
694
+ if disk.get('boot', False):
695
+ return True
696
+
697
+ return False
698
+
699
+ def _ValidateFlags(self):
700
+ """Validate flags coming in before we start building resources.
701
+
702
+ Raises:
703
+ app.UsageError: If service account explicitly given without scopes.
704
+ command_base.CommandError: If scopes contains ' '.
705
+ """
706
+ if (self._flags.service_account and
707
+ self._flags.service_account_scopes):
708
+ # Ensures that the user did not space-delimit his or her scopes
709
+ # list.
710
+ for scope in self._flags.service_account_scopes:
711
+ if ' ' in scope:
712
+ raise command_base.CommandError(
713
+ 'Scopes list must be comma-delimited, not space-delimited.')
714
+ elif self._flags['service_account'].present:
715
+ raise app.UsageError(
716
+ '--service_account given without --service_account_scopes.')
717
+
718
+ if self._flags.wait_until_running and not self._flags.synchronous_mode:
719
+ LOGGER.warn('wait_until_running set. Implying synchronous_mode.')
720
+ self._flags.synchronous_mode = True
721
+
722
+ def _CreateDiskFromImageRequest(self, disk_name):
723
+ """Build a request that creates disk from source image.
724
+
725
+ Args:
726
+ disk_name: Name of the disk.
727
+
728
+ Returns:
729
+ The prepared disk insert request.
730
+ """
731
+
732
+ disk_resource = {
733
+ 'kind': self._GetResourceApiKind('instance'),
734
+ 'name': disk_name,
735
+ 'description': 'Persistent boot disk created from %s.' % (
736
+ self._flags.image),
737
+ 'zone': self.NormalizeTopLevelResourceName(self._project, 'zones',
738
+ self._flags.zone),
739
+ }
740
+ source_image_url = self.NormalizeGlobalResourceName(self._project, 'images',
741
+ self._flags.image)
742
+ kwargs = {
743
+ 'project': self._project,
744
+ 'body': disk_resource,
745
+ 'sourceImage': source_image_url
746
+ }
747
+ if self._IsUsingAtLeastApiVersion('v1beta14'):
748
+ kwargs['zone'] = disk_resource['zone'].split('/')[-1]
749
+ del disk_resource['zone']
750
+ return self._disks_api.insert(**kwargs)
751
+
752
+ def _BuildRequestWithMetadata(self, instance_name, instance_metadata, disks):
753
+ """Build a request to add the specified instance, given the ssh keys for it.
754
+
755
+ Args:
756
+ instance_name: Name of the instance to build a request for.
757
+ instance_metadata: The metadata to be passed to the VM. This is in the
758
+ form of [{'key': <key>, 'value': <value>}] form, ready to be
759
+ sent to the server.
760
+ disks: Disks to attach to the instance.
761
+
762
+ Returns:
763
+ The prepared instance request.
764
+ """
765
+ instance_resource = {
766
+ 'kind': self._GetResourceApiKind('instance'),
767
+ 'name': self.DenormalizeResourceName(instance_name),
768
+ 'description': self._flags.description,
769
+ 'networkInterfaces': [],
770
+ 'disks': disks,
771
+ 'metadata': [],
772
+ }
773
+
774
+ if self._flags.image:
775
+ instance_resource['image'] = self.NormalizeGlobalResourceName(
776
+ self._project, 'images', self._flags.image)
777
+
778
+ if self._flags.kernel:
779
+ instance_resource['kernel'] = self.NormalizeGlobalResourceName(
780
+ self._project, 'kernels', self._flags.kernel)
781
+
782
+ if self._flags.machine_type:
783
+ instance_resource['machineType'] = self.NormalizeGlobalResourceName(
784
+ self._project, 'machine-types', self._flags.machine_type)
785
+
786
+
787
+ instance_resource['zone'] = self.NormalizeTopLevelResourceName(
788
+ self._project, 'zones', self._flags.zone)
789
+
790
+ if self._flags.network:
791
+ network_interface = {
792
+ 'network': self.NormalizeGlobalResourceName(self._project, 'networks',
793
+ self._flags.network)
794
+ }
795
+ if self._flags.internal_ip_address:
796
+ network_interface['networkIP'] = self._flags.internal_ip_address
797
+ external_ip_address = self._flags.external_ip_address
798
+ if external_ip_address and external_ip_address.lower() != 'none':
799
+ access_config = {
800
+ 'name': self.DEFAULT_ACCESS_CONFIG_NAME,
801
+ 'type': self.ONE_TO_ONE_NAT_ACCESS_CONFIG_TYPE,
802
+ }
803
+ if external_ip_address.lower() != self.EPHEMERAL_ACCESS_CONFIG_NAT_IP:
804
+ access_config['natIP'] = self._flags.external_ip_address
805
+
806
+ network_interface['accessConfigs'] = [access_config]
807
+
808
+ instance_resource['networkInterfaces'].append(network_interface)
809
+
810
+ metadata_subresource = {
811
+ 'kind': self._GetResourceApiKind('metadata'),
812
+ 'items': []}
813
+
814
+ metadata_subresource['items'].extend(instance_metadata)
815
+ instance_resource['metadata'] = metadata_subresource
816
+
817
+ if self._flags.service_account and (
818
+ len(self._flags.service_account_scopes)):
819
+ instance_resource['serviceAccounts'] = []
820
+ expanded_scopes = scopes.ExpandScopeAliases(
821
+ self._flags.service_account_scopes)
822
+ instance_resource['serviceAccounts'].append({
823
+ 'email': self._flags.service_account,
824
+ 'scopes': expanded_scopes})
825
+
826
+ instance_tags = sorted(set(self._flags.tags))
827
+ if self._IsUsingAtLeastApiVersion('v1beta14'):
828
+ instance_tags = {'items': sorted(set(self._flags.tags))}
829
+ instance_resource['tags'] = instance_tags
830
+ kwargs = {
831
+ 'project': self._project,
832
+ 'body': instance_resource,
833
+ }
834
+ if self._IsUsingAtLeastApiVersion('v1beta14'):
835
+ kwargs['zone'] = self.DenormalizeResourceName(self._flags.zone)
836
+ del instance_resource['zone']
837
+
838
+ return self._instances_api.insert(**kwargs)
839
+
840
+
841
+ class GetInstance(InstanceCommand):
842
+ """Get a machine instance."""
843
+
844
+ positional_args = '<instance-name>'
845
+
846
+ def Handle(self, instance_name):
847
+ """Get the specified instance.
848
+
849
+ Args:
850
+ instance_name: The name of the instance to get.
851
+
852
+ Returns:
853
+ The result of getting the instance.
854
+ """
855
+ instance_request = self._instances_api.get(
856
+ **self._PrepareRequestArgs(instance_name))
857
+
858
+ return instance_request.execute()
859
+
860
+
861
+ class DeleteInstance(InstanceCommand):
862
+ """Delete one or more VM instances.
863
+
864
+ If multiple instance names are specified, the instances will be deleted
865
+ in parallel.
866
+ """
867
+
868
+ positional_args = '<instance-name-1> ... <instance-name-n>'
869
+ safety_prompt = 'Delete instance'
870
+
871
+ def Handle(self, *instance_names):
872
+ """Delete the specified instances.
873
+
874
+ Args:
875
+ *instance_names: Names of the instances to delete.
876
+
877
+ Returns:
878
+ The result of deleting the instance.
879
+ """
880
+ if self._IsUsingAtLeastApiVersion('v1beta14') and not self._flags.zone:
881
+ if len(instance_names) > 1:
882
+ self._flags.zone = self._GetZone()
883
+ else:
884
+ self._flags.zone = self.GetZoneForResource(self._instances_api,
885
+ instance_names[0])
886
+
887
+ requests = []
888
+ for instance_name in instance_names:
889
+ requests.append(self._instances_api.delete(
890
+ **self._PrepareRequestArgs(instance_name)))
891
+ (results, exceptions) = self.ExecuteRequests(requests)
892
+ return (self.MakeListResult(results, 'operationList'), exceptions)
893
+
894
+
895
+ class ListInstances(InstanceCommand, command_base.GoogleComputeListCommand):
896
+ """List the instances for a project."""
897
+
898
+ is_global_level_collection = False
899
+ is_zone_level_collection = True
900
+
901
+ def ListFunc(self):
902
+ """Returns the function for listing instances."""
903
+ if self._IsUsingAtLeastApiVersion('v1beta14'):
904
+ return None
905
+ return self._instances_api.list
906
+
907
+ def ListZoneFunc(self):
908
+ """Returns the function for listing instances in a zone."""
909
+ return self._instances_api.list
910
+
911
+
912
+ class AddAccessConfig(InstanceCommand):
913
+ """Adds an access config to an instance's network interface."""
914
+
915
+ positional_args = '<instance-name>'
916
+
917
+ def __init__(self, name, flag_values):
918
+ super(AddAccessConfig, self).__init__(name, flag_values)
919
+
920
+ flags.DEFINE_string('network_interface_name',
921
+ self.DEFAULT_NETWORK_INTERFACE_NAME,
922
+ 'The name of the instance\'s network interface to '
923
+ 'which to add the new access config.',
924
+ flag_values=flag_values)
925
+
926
+ flags.DEFINE_string('access_config_name',
927
+ self.DEFAULT_ACCESS_CONFIG_NAME,
928
+ 'The name of the new access config.',
929
+ flag_values=flag_values)
930
+
931
+ flags.DEFINE_string('access_config_type',
932
+ self.ONE_TO_ONE_NAT_ACCESS_CONFIG_TYPE,
933
+ 'The type of the new access config. Currently only '
934
+ 'type "ONE_TO_ONE_NAT" is supported.',
935
+ flag_values=flag_values)
936
+
937
+ flags.DEFINE_string('access_config_nat_ip',
938
+ self.EPHEMERAL_ACCESS_CONFIG_NAT_IP,
939
+ 'The external NAT IP of the new access config. The '
940
+ 'default value "ephemeral" indicates the service '
941
+ 'should choose an available ephemeral IP. If an '
942
+ 'explicit IP is given, that IP must be reserved by '
943
+ 'the project and not yet assigned to another instance.',
944
+ flag_values=flag_values)
945
+
946
+ def Handle(self, instance_name):
947
+ """Adds an access config to an instance's network interface.
948
+
949
+ Args:
950
+ instance_name: The instance name to which to add the new access config.
951
+
952
+ Returns:
953
+ An operation resource.
954
+ """
955
+ access_config_resource = {
956
+ 'name': self._flags.access_config_name,
957
+ 'type': self._flags.access_config_type,
958
+ }
959
+ if (self._flags.access_config_nat_ip.lower() !=
960
+ self.EPHEMERAL_ACCESS_CONFIG_NAT_IP):
961
+ access_config_resource['natIP'] = self._flags.access_config_nat_ip
962
+
963
+ add_access_config_request = self._instances_api.addAccessConfig(
964
+ **self._PrepareRequestArgs(
965
+ instance_name,
966
+ network_interface=self._flags.network_interface_name,
967
+ body=access_config_resource))
968
+ return add_access_config_request.execute()
969
+
970
+
971
+ class DeleteAccessConfig(InstanceCommand):
972
+ """Deletes an access config from an instance's network interface."""
973
+
974
+ positional_args = '<instance-name>'
975
+
976
+ def __init__(self, name, flag_values):
977
+ super(DeleteAccessConfig, self).__init__(name, flag_values)
978
+
979
+ flags.DEFINE_string('network_interface_name',
980
+ self.DEFAULT_NETWORK_INTERFACE_NAME,
981
+ 'The name of the instance\'s network interface from '
982
+ 'which to delete the access config.',
983
+ flag_values=flag_values)
984
+
985
+ flags.DEFINE_string('access_config_name',
986
+ self.DEFAULT_ACCESS_CONFIG_NAME,
987
+ 'The name of the access config to delete.',
988
+ flag_values=flag_values)
989
+
990
+ def Handle(self, instance_name):
991
+ """Deletes an access config from an instance's network interface.
992
+
993
+ Args:
994
+ instance_name: The instance name from which to delete the access config.
995
+
996
+ Returns:
997
+ An operation resource.
998
+ """
999
+ delete_access_config_request = self._instances_api.deleteAccessConfig(
1000
+ **self._PrepareRequestArgs(
1001
+ instance_name,
1002
+ network_interface=self._flags.network_interface_name,
1003
+ access_config=self._flags.access_config_name))
1004
+ return delete_access_config_request.execute()
1005
+
1006
+
1007
+ class SshInstanceBase(InstanceCommand):
1008
+ """Base class for SSH-based commands."""
1009
+
1010
+ # We want everything after 'ssh <instance>' to be passed on to the
1011
+ # ssh command in question. As such, all arguments to the utility
1012
+ # must come before the 'ssh' command.
1013
+ sort_args_and_flags = False
1014
+
1015
+ def __init__(self, name, flag_values):
1016
+ super(SshInstanceBase, self).__init__(name, flag_values)
1017
+
1018
+ flags.DEFINE_integer(
1019
+ 'ssh_port',
1020
+ 22,
1021
+ 'TCP port to connect to',
1022
+ flag_values=flag_values)
1023
+ flags.DEFINE_multistring(
1024
+ 'ssh_arg',
1025
+ [],
1026
+ 'Additional arguments to pass to ssh',
1027
+ flag_values=flag_values)
1028
+ flags.DEFINE_integer(
1029
+ 'ssh_key_push_wait_time',
1030
+ 300, # 5 minutes
1031
+ 'Number of seconds to wait for updates to project-wide ssh keys '
1032
+ 'to cascade to the instances within the project',
1033
+ flag_values=flag_values)
1034
+
1035
+ def PrintResult(self, _):
1036
+ """Override the PrintResult to be a noop."""
1037
+ pass
1038
+
1039
+ def _GetInstanceResource(self, instance_name):
1040
+ """Get the instance resource. This is the dictionary returned by the API.
1041
+
1042
+ Args:
1043
+ instance_name: The name of the instance to retrieve the ssh address for.
1044
+
1045
+ Returns:
1046
+ The data for the instance resource as returned by the API.
1047
+
1048
+ Raises:
1049
+ command_base.CommandError: If the instance does not exist.
1050
+ """
1051
+ request = self._instances_api.get(
1052
+ **self._PrepareRequestArgs(instance_name))
1053
+ result = request.execute()
1054
+ if not result:
1055
+ raise command_base.CommandError(
1056
+ 'Unable to find the instance %s.' % (instance_name))
1057
+ return result
1058
+
1059
+ def _GetSshAddress(self, instance_resource):
1060
+ """Retrieve the ssh address from the passed instance resource data.
1061
+
1062
+ Args:
1063
+ instance_resource: The resource data of the instance for which
1064
+ to retrieve the ssh address.
1065
+
1066
+ Returns:
1067
+ The ssh address and port.
1068
+
1069
+ Raises:
1070
+ command_base.CommandError: If the instance has no external address.
1071
+ """
1072
+ external_addresses = self._ExtractExternalIpFromInstanceRecord(
1073
+ instance_resource)
1074
+ if len(external_addresses) < 1:
1075
+ raise command_base.CommandError(
1076
+ 'Cannot connect to an instance with no external address')
1077
+
1078
+ return (external_addresses[0], self._flags.ssh_port)
1079
+
1080
+ def _EnsureSshable(self, instance_resource):
1081
+ """Ensure that the user can ssh into the specified instance.
1082
+
1083
+ This method checks if the instance has SSH keys defined for it, and if
1084
+ it does not this makes sure the enclosing project contains a metadata
1085
+ entry for the user's public ssh key.
1086
+
1087
+ If the project is updated to add the user's ssh key, then this method
1088
+ waits for the amount of time specified by the wait_time_for_ssh_key_push
1089
+ flag for the change to cascade down to the instance.
1090
+
1091
+ Args:
1092
+ instance_resource: The resource data for the instance to which to connect.
1093
+
1094
+ Raises:
1095
+ command_base.CommandError: If the instance is not in the RUNNING state.
1096
+ """
1097
+ instance_status = instance_resource.get('status')
1098
+ if instance_status != 'RUNNING':
1099
+ raise command_base.CommandError(
1100
+ 'Cannot connect to the instance since its current status is %s.'
1101
+ % instance_status)
1102
+
1103
+ instance_metadata = instance_resource.get('metadata', {})
1104
+
1105
+ instance_ssh_key_entries = (
1106
+ [entry for entry in instance_metadata.get(
1107
+ 'items', [])
1108
+ if entry.get('key') == 'sshKeys'])
1109
+
1110
+ if not instance_ssh_key_entries:
1111
+ if self._AddComputeKeyToProject():
1112
+ wait_time = self._flags.ssh_key_push_wait_time
1113
+ LOGGER.info('Updated project with new ssh key. It can take several '
1114
+ 'minutes for the instance to pick up the key.')
1115
+ LOGGER.info('Waiting %s seconds before attempting to connect.',
1116
+ wait_time)
1117
+ time.sleep(wait_time)
1118
+
1119
+ def _BuildSshCmd(self, instance_resource, command, args):
1120
+ """Builds the given SSH-based command line with the given arguments.
1121
+
1122
+ A complete SSH-based command line is built from the given command,
1123
+ any common arguments, and the arguments provided. The value of
1124
+ each argument is formatted using a dictionary that contains the
1125
+ following keys: host and port.
1126
+
1127
+ Args:
1128
+ instance_resource: The resource data of the instance for which
1129
+ to build the ssh command.
1130
+ command: the ssh-based command to run (e.g. ssh or scp)
1131
+ args: arguments for the command
1132
+
1133
+ Returns:
1134
+ The command line used to perform the requested ssh operation.
1135
+
1136
+ Raises:
1137
+ IOError: An error occured accessing SSH details.
1138
+ """
1139
+ (host, port) = self._GetSshAddress(instance_resource)
1140
+ values = {'host': host,
1141
+ 'port': port,
1142
+ 'user': self._flags.ssh_user}
1143
+
1144
+ command_line = [
1145
+ command,
1146
+ '-o', 'UserKnownHostsFile=/dev/null',
1147
+ '-o', 'CheckHostIP=no',
1148
+ '-o', 'StrictHostKeyChecking=no',
1149
+ '-i', self._flags.private_key_file
1150
+ ] + self._flags.ssh_arg
1151
+
1152
+ if LOGGER.level <= logging.DEBUG:
1153
+ command_line.append('-v')
1154
+
1155
+ for arg in args:
1156
+ command_line.append(arg % values)
1157
+
1158
+ return command_line
1159
+
1160
+ def _RunSshCmd(self, instance_name, command, args):
1161
+ """Run the given SSH-based command line with the given arguments.
1162
+
1163
+ The specified SSH-base command is run for the arguments provided.
1164
+ The value of each argument is formatted using a dictionary that
1165
+ contains the following keys: host and port.
1166
+
1167
+ Args:
1168
+ instance_name: The name of the instance for which to run the ssh command.
1169
+ command: the ssh-based command to run (e.g. ssh or scp)
1170
+ args: arguments for the command
1171
+
1172
+ Raises:
1173
+ IOError: An error occured accessing SSH details.
1174
+ """
1175
+ instance_resource = self._GetInstanceResource(instance_name)
1176
+ command_line = self._BuildSshCmd(instance_resource, command, args)
1177
+ try:
1178
+ self._EnsureSshable(instance_resource)
1179
+ except ssh_keys.UserSetupError as e:
1180
+ LOGGER.warn('Could not generate compute ssh key: %s', e)
1181
+ return
1182
+
1183
+ LOGGER.info('Running command line: %s', ' '.join(command_line))
1184
+ try:
1185
+ os.execvp(command, command_line)
1186
+ except OSError as e:
1187
+ LOGGER.error('There was a problem executing the command: %s', e)
1188
+
1189
+
1190
+ class SshToInstance(SshInstanceBase):
1191
+ """Ssh to an instance."""
1192
+
1193
+ positional_args = '<instance-name> <ssh-args>'
1194
+
1195
+ def _GenerateSshArgs(self, *argv):
1196
+ """Generates the command line arguments for the ssh command.
1197
+
1198
+ Args:
1199
+ *argv: List of additional ssh command line args, if any.
1200
+
1201
+ Returns:
1202
+ The complete ssh argument list.
1203
+ """
1204
+ ssh_args = ['-A', '-p', '%(port)d', '%(user)s@%(host)s', '--']
1205
+
1206
+ escaped_args = [a.replace('%', '%%') for a in argv]
1207
+ ssh_args.extend(escaped_args)
1208
+
1209
+ return ssh_args
1210
+
1211
+ def Handle(self, instance_name, *argv):
1212
+ """SSH into the instance.
1213
+
1214
+ Args:
1215
+ instance_name: The name of the instance to ssh to.
1216
+ *argv: The remaining unhandled arguments.
1217
+
1218
+ Returns:
1219
+ The result of the ssh command
1220
+ """
1221
+ ssh_args = self._GenerateSshArgs(*argv)
1222
+ self._RunSshCmd(instance_name, 'ssh', ssh_args)
1223
+
1224
+
1225
+ class PushToInstance(SshInstanceBase):
1226
+ """Push one or more files to an instance."""
1227
+
1228
+ positional_args = '<instance-name> <file-1> ... <file-n> <destination>'
1229
+
1230
+ def _GenerateScpArgs(self, *argv):
1231
+ """Generates the command line arguments for the scp command.
1232
+
1233
+ Args:
1234
+ *argv: List of files to push and instance-relative destination.
1235
+
1236
+ Returns:
1237
+ The scp argument list.
1238
+
1239
+ Raises:
1240
+ command_base.CommandError: If an invalid number of arguments are passed
1241
+ in.
1242
+ """
1243
+ if len(argv) < 2:
1244
+ raise command_base.CommandError('Invalid number of arguments passed.')
1245
+
1246
+ scp_args = ['-r', '-P', '%(port)d', '--']
1247
+
1248
+ escaped_args = [a.replace('%', '%%') for a in argv]
1249
+ scp_args.extend(escaped_args[0:-1])
1250
+ scp_args.append('%(user)s@%(host)s:' + escaped_args[-1])
1251
+
1252
+ return scp_args
1253
+
1254
+ def Handle(self, instance_name, *argv):
1255
+ """Pushes one or more files into the instance.
1256
+
1257
+ Args:
1258
+ instance_name: The name of the instance to push files to.
1259
+ *argv: The remaining unhandled arguments.
1260
+
1261
+ Returns:
1262
+ The result of the scp command
1263
+
1264
+ Raises:
1265
+ command_base.CommandError: If an invalid number of arguments are passed
1266
+ in.
1267
+ """
1268
+ scp_args = self._GenerateScpArgs(*argv)
1269
+ self._RunSshCmd(instance_name, 'scp', scp_args)
1270
+
1271
+
1272
+ class PullFromInstance(SshInstanceBase):
1273
+ """Pull one or more files from an instance."""
1274
+
1275
+ positional_args = '<instance-name> <file-1> ... <file-n> <destination>'
1276
+
1277
+ def _GenerateScpArgs(self, *argv):
1278
+ """Generates the command line arguments for the scp command.
1279
+
1280
+ Args:
1281
+ *argv: List of files to pull and local-relative destination.
1282
+
1283
+ Returns:
1284
+ The scp argument list.
1285
+
1286
+ Raises:
1287
+ command_base.CommandError: If an invalid number of arguments are passed
1288
+ in.
1289
+ """
1290
+ if len(argv) < 2:
1291
+ raise command_base.CommandError('Invalid number of arguments passed.')
1292
+
1293
+ scp_args = ['-r', '-P', '%(port)d', '--']
1294
+
1295
+ escaped_args = [a.replace('%', '%%') for a in argv]
1296
+ for arg in escaped_args[0:-1]:
1297
+ scp_args.append('%(user)s@%(host)s:' + arg)
1298
+ scp_args.append(escaped_args[-1])
1299
+
1300
+ return scp_args
1301
+
1302
+ def Handle(self, instance_name, *argv):
1303
+ """Pulls one or more files from the instance.
1304
+
1305
+ Args:
1306
+ instance_name: The name of the instance to pull files from.
1307
+ *argv: The remaining unhandled arguments.
1308
+
1309
+ Returns:
1310
+ The result of the scp command
1311
+
1312
+ Raises:
1313
+ command_base.CommandError: If an invalid number of arguments are passed
1314
+ in.
1315
+ """
1316
+ scp_args = self._GenerateScpArgs(*argv)
1317
+ self._RunSshCmd(instance_name, 'scp', scp_args)
1318
+
1319
+
1320
+ class GetSerialPortOutput(InstanceCommand):
1321
+ """Get the output of an instance's serial port."""
1322
+
1323
+ positional_args = '<instance-name>'
1324
+
1325
+ def Handle(self, instance_name):
1326
+ """Get the specified instance's serial port output.
1327
+
1328
+ Args:
1329
+ instance_name: The name of the instance.
1330
+
1331
+ Returns:
1332
+ The output of the instance's serial port.
1333
+ """
1334
+ if self._IsUsingAtLeastApiVersion('v1beta13'):
1335
+ instance_request = self._instances_api.getSerialPortOutput(
1336
+ **self._PrepareRequestArgs(instance_name))
1337
+
1338
+ return instance_request.execute()
1339
+ else:
1340
+ raise app.UsageError(
1341
+ 'Serial port output is only supported in v1beta13 and above.')
1342
+
1343
+ def PrintResult(self, result):
1344
+ """Override the PrintResult to be a noop."""
1345
+
1346
+ if self._flags.print_json:
1347
+ super(GetSerialPortOutput, self).PrintResult(result)
1348
+ else:
1349
+ print result['contents']
1350
+
1351
+
1352
+
1353
+
1354
+ class OptimisticallyLockedInstanceCommand(InstanceCommand):
1355
+ """Base class for instance commands that require a fingerprint."""
1356
+
1357
+ def __init__(self, name, flag_values):
1358
+ super(OptimisticallyLockedInstanceCommand, self).__init__(name, flag_values)
1359
+
1360
+ flags.DEFINE_string('fingerprint',
1361
+ None,
1362
+ 'Fingerprint of the data to be overwritten. '
1363
+ 'This fingerprint provides optimistic locking--'
1364
+ 'data will only be set if the given fingerprint '
1365
+ 'matches the state of the data prior to this request.',
1366
+ flag_values=flag_values)
1367
+
1368
+ def Handle(self, *args, **kwargs):
1369
+ """Invokes the HandleCommand method of the subclass."""
1370
+ if not self._flags.fingerprint:
1371
+ raise app.UsageError('You must provide a fingerprint with your request.')
1372
+ return self.HandleCommand(*args, **kwargs)
1373
+
1374
+
1375
+ class SetMetadata(OptimisticallyLockedInstanceCommand):
1376
+ """Sets instance metadata and sends new metadata to instances.
1377
+
1378
+ This method overwrites existing instance metadata with new metadata.
1379
+ Common metadata (project-wide) is preserved.
1380
+
1381
+ For example, running:
1382
+
1383
+ gcutil --project=<project-name> setinstancemetadata my-instance \
1384
+ --metadata="key1:value1" \
1385
+ --fingerprint=<original-fingerprint>
1386
+ ...
1387
+ gcutil --project=<project-name> setinstancemetadata my-instance \
1388
+ --metadata="key2:value2" \
1389
+ --fingerprint=<new-fingerprint>
1390
+
1391
+ will result in 'my-instance' having 'key2:value2' as its metadata.
1392
+ """
1393
+
1394
+ positional_args = '<instance-name>'
1395
+
1396
+ def __init__(self, name, flag_values):
1397
+ super(SetMetadata, self).__init__(name, flag_values)
1398
+
1399
+ flags.DEFINE_bool('force',
1400
+ None,
1401
+ 'Set new metadata even if the key "sshKeys" will '
1402
+ 'no longer be present.',
1403
+ flag_values=flag_values,
1404
+ short_name='f')
1405
+ self._metadata_flags_processor = metadata.MetadataFlagsProcessor(
1406
+ flag_values)
1407
+
1408
+ def HandleCommand(self, instance_name):
1409
+ """Set instance-specific metadata.
1410
+
1411
+ Args:
1412
+ instance_name: The name of the instance scoping this request.
1413
+
1414
+ Returns:
1415
+ An operation resource.
1416
+ """
1417
+ new_metadata = self._metadata_flags_processor.GatherMetadata()
1418
+ if not self._flags.force:
1419
+ new_keys = set([entry['key'] for entry in new_metadata])
1420
+ get_project = self._projects_api.get(project=self._project)
1421
+ project_resource = get_project.execute()
1422
+ project_metadata = project_resource.get('commonInstanceMetadata', {})
1423
+ project_metadata = project_metadata.get('items', [])
1424
+ project_keys = set([entry['key'] for entry in project_metadata])
1425
+
1426
+ get_instance = self._instances_api.get(
1427
+ **self._PrepareRequestArgs(instance_name))
1428
+ instance_resource = get_instance.execute()
1429
+ instance_metadata = instance_resource.get('metadata', {})
1430
+ instance_metadata = instance_metadata.get('items', [])
1431
+ instance_keys = set([entry['key'] for entry in instance_metadata])
1432
+
1433
+ if ('sshKeys' in instance_keys and 'sshKeys' not in new_keys
1434
+ and 'sshKeys' not in project_keys):
1435
+ raise command_base.CommandError(
1436
+ 'Discarding update that would have erased instance sshKeys.'
1437
+ '\n\nRe-run with the -f flag to force the update.')
1438
+
1439
+ metadata_resource = {'kind': self._GetResourceApiKind('metadata'),
1440
+ 'items': new_metadata,
1441
+ 'fingerprint': self._flags.fingerprint}
1442
+
1443
+ set_metadata_request = self._instances_api.setMetadata(
1444
+ **self._PrepareRequestArgs(instance_name, body=metadata_resource))
1445
+ return set_metadata_request.execute()
1446
+
1447
+
1448
+ class SetTags(OptimisticallyLockedInstanceCommand):
1449
+ """Sets instance tags and sends new tags to the instance.
1450
+
1451
+ This method overwrites existing instance tags.
1452
+
1453
+ For example, running:
1454
+
1455
+ gcutil --project=<project-name> setinstancetags my-instance \
1456
+ --tags="tag-1" \
1457
+ --fingerprint=<original-fingerprint>
1458
+ ...
1459
+ gcutil --project=<project-name> setinstancetags my-instance \
1460
+ --tags="tag-2,tag-3" \
1461
+ --fingerprint=<new-fingerprint>
1462
+
1463
+ will result in 'my-instance' having tags 'tag-2' and 'tag-3'.
1464
+ """
1465
+
1466
+ def __init__(self, name, flag_values):
1467
+ super(SetTags, self).__init__(name, flag_values)
1468
+
1469
+ flags.DEFINE_list('tags',
1470
+ [],
1471
+ 'A set of tags applied to this instance. Used for '
1472
+ 'filtering and to configure network firewall rules '
1473
+ '(comma separated).',
1474
+ flag_values=flag_values)
1475
+
1476
+ def HandleCommand(self, instance_name):
1477
+ """Set instance tags.
1478
+
1479
+ Args:
1480
+ instance_name: The name of the instance scoping this request.
1481
+
1482
+ Returns:
1483
+ An operation resource.
1484
+ """
1485
+ tag_resource = {'items': sorted(set(self._flags.tags)),
1486
+ 'fingerprint': self._flags.fingerprint}
1487
+ set_tags_request = self._instances_api.setTags(
1488
+ **self._PrepareRequestArgs(instance_name, body=tag_resource))
1489
+ return set_tags_request.execute()
1490
+
1491
+
1492
+ def AddCommands():
1493
+ """Add all of the instance related commands."""
1494
+
1495
+ appcommands.AddCmd('addinstance', AddInstance)
1496
+ appcommands.AddCmd('getinstance', GetInstance)
1497
+ appcommands.AddCmd('deleteinstance', DeleteInstance)
1498
+ appcommands.AddCmd('listinstances', ListInstances)
1499
+ appcommands.AddCmd('addaccessconfig', AddAccessConfig)
1500
+ appcommands.AddCmd('deleteaccessconfig', DeleteAccessConfig)
1501
+ appcommands.AddCmd('ssh', SshToInstance)
1502
+ appcommands.AddCmd('push', PushToInstance)
1503
+ appcommands.AddCmd('pull', PullFromInstance)
1504
+ appcommands.AddCmd('getserialportoutput', GetSerialPortOutput)
1505
+ appcommands.AddCmd('setinstancemetadata', SetMetadata)
1506
+ appcommands.AddCmd('setinstancetags', SetTags)