gcloud 0.0.2 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (118) hide show
  1. data.tar.gz.sig +2 -3
  2. data/CHANGELOG +4 -0
  3. data/LICENSE +674 -0
  4. data/Manifest +111 -0
  5. data/README.md +4 -3
  6. data/bin/gcutil +53 -0
  7. data/gcloud.gemspec +4 -3
  8. data/packages/gcutil-1.7.1/CHANGELOG +197 -0
  9. data/packages/gcutil-1.7.1/LICENSE +202 -0
  10. data/packages/gcutil-1.7.1/VERSION +1 -0
  11. data/packages/gcutil-1.7.1/gcutil +53 -0
  12. data/packages/gcutil-1.7.1/lib/google_api_python_client/LICENSE +23 -0
  13. data/packages/gcutil-1.7.1/lib/google_api_python_client/apiclient/__init__.py +1 -0
  14. data/packages/gcutil-1.7.1/lib/google_api_python_client/apiclient/discovery.py +743 -0
  15. data/packages/gcutil-1.7.1/lib/google_api_python_client/apiclient/errors.py +123 -0
  16. data/packages/gcutil-1.7.1/lib/google_api_python_client/apiclient/ext/__init__.py +0 -0
  17. data/packages/gcutil-1.7.1/lib/google_api_python_client/apiclient/http.py +1443 -0
  18. data/packages/gcutil-1.7.1/lib/google_api_python_client/apiclient/mimeparse.py +172 -0
  19. data/packages/gcutil-1.7.1/lib/google_api_python_client/apiclient/model.py +385 -0
  20. data/packages/gcutil-1.7.1/lib/google_api_python_client/apiclient/schema.py +303 -0
  21. data/packages/gcutil-1.7.1/lib/google_api_python_client/oauth2client/__init__.py +1 -0
  22. data/packages/gcutil-1.7.1/lib/google_api_python_client/oauth2client/anyjson.py +32 -0
  23. data/packages/gcutil-1.7.1/lib/google_api_python_client/oauth2client/appengine.py +528 -0
  24. data/packages/gcutil-1.7.1/lib/google_api_python_client/oauth2client/client.py +1139 -0
  25. data/packages/gcutil-1.7.1/lib/google_api_python_client/oauth2client/clientsecrets.py +105 -0
  26. data/packages/gcutil-1.7.1/lib/google_api_python_client/oauth2client/crypt.py +244 -0
  27. data/packages/gcutil-1.7.1/lib/google_api_python_client/oauth2client/django_orm.py +124 -0
  28. data/packages/gcutil-1.7.1/lib/google_api_python_client/oauth2client/file.py +107 -0
  29. data/packages/gcutil-1.7.1/lib/google_api_python_client/oauth2client/locked_file.py +343 -0
  30. data/packages/gcutil-1.7.1/lib/google_api_python_client/oauth2client/multistore_file.py +379 -0
  31. data/packages/gcutil-1.7.1/lib/google_api_python_client/oauth2client/tools.py +174 -0
  32. data/packages/gcutil-1.7.1/lib/google_api_python_client/uritemplate/__init__.py +147 -0
  33. data/packages/gcutil-1.7.1/lib/google_apputils/LICENSE +202 -0
  34. data/packages/gcutil-1.7.1/lib/google_apputils/google/__init__.py +3 -0
  35. data/packages/gcutil-1.7.1/lib/google_apputils/google/apputils/__init__.py +3 -0
  36. data/packages/gcutil-1.7.1/lib/google_apputils/google/apputils/app.py +356 -0
  37. data/packages/gcutil-1.7.1/lib/google_apputils/google/apputils/appcommands.py +783 -0
  38. data/packages/gcutil-1.7.1/lib/google_apputils/google/apputils/basetest.py +1260 -0
  39. data/packages/gcutil-1.7.1/lib/google_apputils/google/apputils/datelib.py +421 -0
  40. data/packages/gcutil-1.7.1/lib/google_apputils/google/apputils/debug.py +60 -0
  41. data/packages/gcutil-1.7.1/lib/google_apputils/google/apputils/file_util.py +181 -0
  42. data/packages/gcutil-1.7.1/lib/google_apputils/google/apputils/resources.py +67 -0
  43. data/packages/gcutil-1.7.1/lib/google_apputils/google/apputils/run_script_module.py +217 -0
  44. data/packages/gcutil-1.7.1/lib/google_apputils/google/apputils/setup_command.py +159 -0
  45. data/packages/gcutil-1.7.1/lib/google_apputils/google/apputils/shellutil.py +49 -0
  46. data/packages/gcutil-1.7.1/lib/google_apputils/google/apputils/stopwatch.py +204 -0
  47. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/__init__.py +0 -0
  48. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/auth_helper.py +140 -0
  49. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/auth_helper_test.py +149 -0
  50. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/auto_auth.py +130 -0
  51. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/auto_auth_test.py +75 -0
  52. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/basic_cmds.py +128 -0
  53. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/basic_cmds_test.py +111 -0
  54. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/command_base.py +1808 -0
  55. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/command_base_test.py +1651 -0
  56. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/compute/v1beta13.json +2851 -0
  57. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/compute/v1beta14.json +3361 -0
  58. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/disk_cmds.py +342 -0
  59. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/disk_cmds_test.py +474 -0
  60. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/firewall_cmds.py +344 -0
  61. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/firewall_cmds_test.py +231 -0
  62. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/flags_cache.py +274 -0
  63. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/gcutil +89 -0
  64. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/gcutil_logging.py +69 -0
  65. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/image_cmds.py +262 -0
  66. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/image_cmds_test.py +172 -0
  67. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/instance_cmds.py +1506 -0
  68. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/instance_cmds_test.py +1904 -0
  69. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/kernel_cmds.py +91 -0
  70. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/kernel_cmds_test.py +56 -0
  71. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/machine_type_cmds.py +106 -0
  72. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/machine_type_cmds_test.py +59 -0
  73. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/metadata.py +96 -0
  74. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/metadata_lib.py +357 -0
  75. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/metadata_test.py +84 -0
  76. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/mock_api.py +420 -0
  77. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/mock_metadata.py +58 -0
  78. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/move_cmds.py +824 -0
  79. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/move_cmds_test.py +307 -0
  80. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/network_cmds.py +178 -0
  81. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/network_cmds_test.py +133 -0
  82. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/operation_cmds.py +181 -0
  83. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/operation_cmds_test.py +196 -0
  84. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/path_initializer.py +38 -0
  85. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/project_cmds.py +173 -0
  86. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/project_cmds_test.py +111 -0
  87. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/scopes.py +61 -0
  88. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/scopes_test.py +50 -0
  89. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/snapshot_cmds.py +276 -0
  90. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/snapshot_cmds_test.py +260 -0
  91. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/ssh_keys.py +266 -0
  92. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/ssh_keys_test.py +128 -0
  93. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/table_formatter.py +563 -0
  94. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/thread_pool.py +188 -0
  95. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/thread_pool_test.py +88 -0
  96. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/utils.py +208 -0
  97. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/utils_test.py +193 -0
  98. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/version.py +17 -0
  99. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/version_checker.py +246 -0
  100. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/version_checker_test.py +271 -0
  101. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/zone_cmds.py +151 -0
  102. data/packages/gcutil-1.7.1/lib/google_compute_engine/gcutil/zone_cmds_test.py +60 -0
  103. data/packages/gcutil-1.7.1/lib/httplib2/LICENSE +21 -0
  104. data/packages/gcutil-1.7.1/lib/httplib2/httplib2/__init__.py +1630 -0
  105. data/packages/gcutil-1.7.1/lib/httplib2/httplib2/cacerts.txt +714 -0
  106. data/packages/gcutil-1.7.1/lib/httplib2/httplib2/iri2uri.py +110 -0
  107. data/packages/gcutil-1.7.1/lib/httplib2/httplib2/socks.py +438 -0
  108. data/packages/gcutil-1.7.1/lib/iso8601/LICENSE +20 -0
  109. data/packages/gcutil-1.7.1/lib/iso8601/iso8601/__init__.py +1 -0
  110. data/packages/gcutil-1.7.1/lib/iso8601/iso8601/iso8601.py +102 -0
  111. data/packages/gcutil-1.7.1/lib/iso8601/iso8601/test_iso8601.py +111 -0
  112. data/packages/gcutil-1.7.1/lib/python_gflags/AUTHORS +2 -0
  113. data/packages/gcutil-1.7.1/lib/python_gflags/LICENSE +28 -0
  114. data/packages/gcutil-1.7.1/lib/python_gflags/gflags.py +2862 -0
  115. data/packages/gcutil-1.7.1/lib/python_gflags/gflags2man.py +544 -0
  116. data/packages/gcutil-1.7.1/lib/python_gflags/gflags_validators.py +187 -0
  117. metadata +118 -5
  118. metadata.gz.sig +0 -0
@@ -0,0 +1,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)