3
# Author: Vaidas Jablonskis <jablonskis@gmail.com>
5
# This program is free software: you can redistribute it and/or modify
6
# it under the terms of the GNU General Public License version 3, as
7
# published by the Free Software Foundation.
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
# GNU General Public License for more details.
14
# You should have received a copy of the GNU General Public License
15
# along with this program. If not, see <http://www.gnu.org/licenses/>.
18
from base64 import b64decode
20
from cloudinit import log as logging
21
from cloudinit import sources
22
from cloudinit import url_helper
23
from cloudinit import util
25
LOG = logging.getLogger(__name__)
28
'metadata_url': 'http://metadata.google.internal/computeMetadata/v1/'
30
REQUIRED_FIELDS = ('instance-id', 'availability-zone', 'local-hostname')
33
class GoogleMetadataFetcher(object):
34
headers = {'X-Google-Metadata-Request': True}
36
def __init__(self, metadata_address):
37
self.metadata_address = metadata_address
39
def get_value(self, path, is_text):
42
resp = url_helper.readurl(url=self.metadata_address + path,
44
except url_helper.UrlError as exc:
45
msg = "url %s raised exception %s"
46
LOG.debug(msg, path, exc)
50
value = util.decode_binary(resp.contents)
54
LOG.debug("url %s returned code %s", path, resp.code)
58
class DataSourceGCE(sources.DataSource):
59
def __init__(self, sys_cfg, distro, paths):
60
sources.DataSource.__init__(self, sys_cfg, distro, paths)
61
self.metadata = dict()
62
self.ds_cfg = util.mergemanydict([
63
util.get_cfg_by_path(sys_cfg, ["datasource", "GCE"], {}),
65
self.metadata_address = self.ds_cfg['metadata_url']
67
# GCE takes sshKeys attribute in the format of '<user>:<public_key>'
68
# so we have to trim each key to remove the username part
69
def _trim_key(self, public_key):
71
index = public_key.index(':')
73
return public_key[(index + 1):]
78
# url_map: (our-key, path, required, is_text)
80
('instance-id', ('instance/id',), True, True),
81
('availability-zone', ('instance/zone',), True, True),
82
('local-hostname', ('instance/hostname',), True, True),
83
('public-keys', ('project/attributes/sshKeys',
84
'instance/attributes/sshKeys'), False, True),
85
('user-data', ('instance/attributes/user-data',), False, False),
86
('user-data-encoding', ('instance/attributes/user-data-encoding',),
90
# if we cannot resolve the metadata server, then no point in trying
91
if not util.is_resolvable_url(self.metadata_address):
92
LOG.debug("%s is not resolvable", self.metadata_address)
95
metadata_fetcher = GoogleMetadataFetcher(self.metadata_address)
96
# iterate over url_map keys to get metadata items
97
running_on_gce = False
98
for (mkey, paths, required, is_text) in url_map:
101
new_value = metadata_fetcher.get_value(path, is_text)
102
if new_value is not None:
105
running_on_gce = True
106
if required and value is None:
107
msg = "required key %s returned nothing. not GCE"
108
if not running_on_gce:
113
self.metadata[mkey] = value
115
if self.metadata['public-keys']:
116
lines = self.metadata['public-keys'].splitlines()
117
self.metadata['public-keys'] = [self._trim_key(k) for k in lines]
119
if self.metadata['availability-zone']:
120
self.metadata['availability-zone'] = self.metadata[
121
'availability-zone'].split('/')[-1]
123
encoding = self.metadata.get('user-data-encoding')
125
if encoding == 'base64':
126
self.metadata['user-data'] = b64decode(
127
self.metadata['user-data'])
129
LOG.warn('unknown user-data-encoding: %s, ignoring', encoding)
131
return running_on_gce
134
def launch_index(self):
135
# GCE does not provide lauch_index property
138
def get_instance_id(self):
139
return self.metadata['instance-id']
141
def get_public_ssh_keys(self):
142
return self.metadata['public-keys']
144
def get_hostname(self, fqdn=False, resolve_ip=False):
145
# GCE has long FDQN's and has asked for short hostnames
146
return self.metadata['local-hostname'].split('.')[0]
148
def get_userdata_raw(self):
149
return self.metadata['user-data']
152
def availability_zone(self):
153
return self.metadata['availability-zone']
157
return self.availability_zone.rsplit('-', 1)[0]
159
# Used to match classes to dependencies
161
(DataSourceGCE, (sources.DEP_FILESYSTEM, sources.DEP_NETWORK)),
165
# Return a list of data sources that match this set of dependencies
166
def get_datasource_list(depends):
167
return sources.list_from_depends(depends, datasources)