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