1# Copyright (C) 2012 Canonical Ltd.
2# Copyright (C) 2012 Cosmin Luta
3# Copyright (C) 2012 Yahoo! Inc.
4# Copyright (C) 2012 Gerard Dethier
5# Copyright (C) 2013 Hewlett-Packard Development Company, L.P.
6#
7# Author: Cosmin Luta <q4break@gmail.com>
8# Author: Scott Moser <scott.moser@canonical.com>
9# Author: Joshua Harlow <harlowja@yahoo-inc.com>
10# Author: Gerard Dethier <g.dethier@gmail.com>
11# Author: Juerg Haefliger <juerg.haefliger@hp.com>
12#
13# This file is part of cloud-init. See LICENSE file for license information.
14
15import os
16from socket import inet_ntoa, getaddrinfo, gaierror
17from struct import pack
18import time
19
20from cloudinit import ec2_utils as ec2
21from cloudinit import log as logging
22from cloudinit.net import dhcp
23from cloudinit import sources
24from cloudinit import url_helper as uhelp
25from cloudinit import subp
26from cloudinit import util
27
28LOG = logging.getLogger(__name__)
29
30
31class CloudStackPasswordServerClient(object):
32    """
33    Implements password fetching from the CloudStack password server.
34
35    http://cloudstack-administration.readthedocs.org/
36       en/latest/templates.html#adding-password-management-to-your-templates
37    has documentation about the system.  This implementation is following that
38    found at
39    https://github.com/shankerbalan/cloudstack-scripts/
40       blob/master/cloud-set-guest-password-debian
41    """
42
43    def __init__(self, virtual_router_address):
44        self.virtual_router_address = virtual_router_address
45
46    def _do_request(self, domu_request):
47        # The password server was in the past, a broken HTTP server, but is now
48        # fixed.  wget handles this seamlessly, so it's easier to shell out to
49        # that rather than write our own handling code.
50        output, _ = subp.subp([
51            'wget', '--quiet', '--tries', '3', '--timeout', '20',
52            '--output-document', '-', '--header',
53            'DomU_Request: {0}'.format(domu_request),
54            '{0}:8080'.format(self.virtual_router_address)
55        ])
56        return output.strip()
57
58    def get_password(self):
59        password = self._do_request('send_my_password')
60        if password in ['', 'saved_password']:
61            return None
62        if password == 'bad_request':
63            raise RuntimeError('Error when attempting to fetch root password.')
64        self._do_request('saved_password')
65        return password
66
67
68class DataSourceCloudStack(sources.DataSource):
69
70    dsname = 'CloudStack'
71
72    # Setup read_url parameters per get_url_params.
73    url_max_wait = 120
74    url_timeout = 50
75
76    def __init__(self, sys_cfg, distro, paths):
77        sources.DataSource.__init__(self, sys_cfg, distro, paths)
78        self.seed_dir = os.path.join(paths.seed_dir, 'cs')
79        # Cloudstack has its metadata/userdata URLs located at
80        # http://<virtual-router-ip>/latest/
81        self.api_ver = 'latest'
82        self.vr_addr = get_vr_address()
83        if not self.vr_addr:
84            raise RuntimeError("No virtual router found!")
85        self.metadata_address = "http://%s/" % (self.vr_addr,)
86        self.cfg = {}
87
88    def wait_for_metadata_service(self):
89        url_params = self.get_url_params()
90
91        if url_params.max_wait_seconds <= 0:
92            return False
93
94        urls = [uhelp.combine_url(self.metadata_address,
95                                  'latest/meta-data/instance-id')]
96        start_time = time.time()
97        url, _response = uhelp.wait_for_url(
98            urls=urls, max_wait=url_params.max_wait_seconds,
99            timeout=url_params.timeout_seconds, status_cb=LOG.warning)
100
101        if url:
102            LOG.debug("Using metadata source: '%s'", url)
103        else:
104            LOG.critical(("Giving up on waiting for the metadata from %s"
105                          " after %s seconds"),
106                         urls, int(time.time() - start_time))
107
108        return bool(url)
109
110    def get_config_obj(self):
111        return self.cfg
112
113    def _get_data(self):
114        seed_ret = {}
115        if util.read_optional_seed(seed_ret, base=(self.seed_dir + "/")):
116            self.userdata_raw = seed_ret['user-data']
117            self.metadata = seed_ret['meta-data']
118            LOG.debug("Using seeded cloudstack data from: %s", self.seed_dir)
119            return True
120        try:
121            if not self.wait_for_metadata_service():
122                return False
123            start_time = time.time()
124            self.userdata_raw = ec2.get_instance_userdata(
125                self.api_ver, self.metadata_address)
126            self.metadata = ec2.get_instance_metadata(self.api_ver,
127                                                      self.metadata_address)
128            LOG.debug("Crawl of metadata service took %s seconds",
129                      int(time.time() - start_time))
130            password_client = CloudStackPasswordServerClient(self.vr_addr)
131            try:
132                set_password = password_client.get_password()
133            except Exception:
134                util.logexc(LOG,
135                            'Failed to fetch password from virtual router %s',
136                            self.vr_addr)
137            else:
138                if set_password:
139                    self.cfg = {
140                        'ssh_pwauth': True,
141                        'password': set_password,
142                        'chpasswd': {
143                            'expire': False,
144                        },
145                    }
146            return True
147        except Exception:
148            util.logexc(LOG, 'Failed fetching from metadata service %s',
149                        self.metadata_address)
150            return False
151
152    def get_instance_id(self):
153        return self.metadata['instance-id']
154
155    @property
156    def availability_zone(self):
157        return self.metadata['availability-zone']
158
159
160def get_data_server():
161    # Returns the metadataserver from dns
162    try:
163        addrinfo = getaddrinfo("data-server", 80)
164    except gaierror:
165        LOG.debug("DNS Entry data-server not found")
166        return None
167    else:
168        return addrinfo[0][4][0]  # return IP
169
170
171def get_default_gateway():
172    # Returns the default gateway ip address in the dotted format.
173    lines = util.load_file("/proc/net/route").splitlines()
174    for line in lines:
175        items = line.split("\t")
176        if items[1] == "00000000":
177            # Found the default route, get the gateway
178            gw = inet_ntoa(pack("<L", int(items[2], 16)))
179            LOG.debug("Found default route, gateway is %s", gw)
180            return gw
181    return None
182
183
184def get_dhclient_d():
185    # find lease files directory
186    supported_dirs = ["/var/lib/dhclient", "/var/lib/dhcp",
187                      "/var/lib/NetworkManager"]
188    for d in supported_dirs:
189        if os.path.exists(d) and len(os.listdir(d)) > 0:
190            LOG.debug("Using %s lease directory", d)
191            return d
192    return None
193
194
195def get_latest_lease(lease_d=None):
196    # find latest lease file
197    if lease_d is None:
198        lease_d = get_dhclient_d()
199    if not lease_d:
200        return None
201    lease_files = os.listdir(lease_d)
202    latest_mtime = -1
203    latest_file = None
204
205    # lease files are named inconsistently across distros.
206    # We assume that 'dhclient6' indicates ipv6 and ignore it.
207    # ubuntu:
208    #   dhclient.<iface>.leases, dhclient.leases, dhclient6.leases
209    # centos6:
210    #   dhclient-<iface>.leases, dhclient6.leases
211    # centos7: ('--' is not a typo)
212    #   dhclient--<iface>.lease, dhclient6.leases
213    for fname in lease_files:
214        if fname.startswith("dhclient6"):
215            # avoid files that start with dhclient6 assuming dhcpv6.
216            continue
217        if not (fname.endswith(".lease") or fname.endswith(".leases")):
218            continue
219
220        abs_path = os.path.join(lease_d, fname)
221        mtime = os.path.getmtime(abs_path)
222        if mtime > latest_mtime:
223            latest_mtime = mtime
224            latest_file = abs_path
225    return latest_file
226
227
228def get_vr_address():
229    # Get the address of the virtual router via dhcp leases
230    # If no virtual router is detected, fallback on default gateway.
231    # See http://docs.cloudstack.apache.org/projects/cloudstack-administration/en/4.8/virtual_machines/user-data.html # noqa
232
233    # Try data-server DNS entry first
234    latest_address = get_data_server()
235    if latest_address:
236        LOG.debug("Found metadata server '%s' via data-server DNS entry",
237                  latest_address)
238        return latest_address
239
240    # Try networkd second...
241    latest_address = dhcp.networkd_get_option_from_leases('SERVER_ADDRESS')
242    if latest_address:
243        LOG.debug("Found SERVER_ADDRESS '%s' via networkd_leases",
244                  latest_address)
245        return latest_address
246
247    # Try dhcp lease files next...
248    lease_file = get_latest_lease()
249    if not lease_file:
250        LOG.debug("No lease file found, using default gateway")
251        return get_default_gateway()
252
253    with open(lease_file, "r") as fd:
254        for line in fd:
255            if "dhcp-server-identifier" in line:
256                words = line.strip(" ;\r\n").split(" ")
257                if len(words) > 2:
258                    dhcptok = words[2]
259                    LOG.debug("Found DHCP identifier %s", dhcptok)
260                    latest_address = dhcptok
261    if not latest_address:
262        # No virtual router found, fallback on default gateway
263        LOG.debug("No DHCP found, using default gateway")
264        return get_default_gateway()
265    return latest_address
266
267
268# Used to match classes to dependencies
269datasources = [
270    (DataSourceCloudStack, (sources.DEP_FILESYSTEM, sources.DEP_NETWORK)),
271]
272
273
274# Return a list of data sources that match this set of dependencies
275def get_datasource_list(depends):
276    return sources.list_from_depends(depends, datasources)
277
278# vi: ts=4 expandtab
279