~ubuntu-branches/ubuntu/saucy/cloud-init/saucy-proposed

« back to all changes in this revision

Viewing changes to .pc/lp-1272115-fix_smartos_compliance.patch/cloudinit/sources/DataSourceSmartOS.py

  • Committer: Scott Moser
  • Date: 2014-03-21 15:53:55 UTC
  • Revision ID: smoser@ubuntu.com-20140321155355-xwl2p4n22201oya8
Tags: 0.7.3-0ubuntu2.1-bzrfix
bzr fix: add .pc/lp-1272115-fix_smartos_compliance.patch

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# vi: ts=4 expandtab
 
2
#
 
3
#    Copyright (C) 2013 Canonical Ltd.
 
4
#
 
5
#    Author: Ben Howard <ben.howard@canonical.com>
 
6
#
 
7
#    This program is free software: you can redistribute it and/or modify
 
8
#    it under the terms of the GNU General Public License version 3, as
 
9
#    published by the Free Software Foundation.
 
10
#
 
11
#    This program is distributed in the hope that it will be useful,
 
12
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
 
13
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
14
#    GNU General Public License for more details.
 
15
#
 
16
#    You should have received a copy of the GNU General Public License
 
17
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
18
#
 
19
#
 
20
#    Datasource for provisioning on SmartOS. This works on Joyent
 
21
#        and public/private Clouds using SmartOS.
 
22
#
 
23
#    SmartOS hosts use a serial console (/dev/ttyS1) on Linux Guests.
 
24
#        The meta-data is transmitted via key/value pairs made by
 
25
#        requests on the console. For example, to get the hostname, you
 
26
#        would send "GET hostname" on /dev/ttyS1.
 
27
#
 
28
 
 
29
 
 
30
import base64
 
31
from cloudinit import log as logging
 
32
from cloudinit import sources
 
33
from cloudinit import util
 
34
import os
 
35
import os.path
 
36
import serial
 
37
 
 
38
 
 
39
LOG = logging.getLogger(__name__)
 
40
 
 
41
SMARTOS_ATTRIB_MAP = {
 
42
    #Cloud-init Key : (SmartOS Key, Strip line endings)
 
43
    'local-hostname': ('hostname', True),
 
44
    'public-keys': ('root_authorized_keys', True),
 
45
    'user-script': ('user-script', False),
 
46
    'user-data': ('user-data', False),
 
47
    'iptables_disable': ('iptables_disable', True),
 
48
    'motd_sys_info': ('motd_sys_info', True),
 
49
    'availability_zone': ('region', True),
 
50
}
 
51
 
 
52
DS_NAME = 'SmartOS'
 
53
DS_CFG_PATH = ['datasource', DS_NAME]
 
54
# BUILT-IN DATASOURCE CONFIGURATION
 
55
#  The following is the built-in configuration. If the values
 
56
#  are not set via the system configuration, then these default
 
57
#  will be used:
 
58
#    serial_device: which serial device to use for the meta-data
 
59
#    seed_timeout: how long to wait on the device
 
60
#    no_base64_decode: values which are not base64 encoded and
 
61
#            are fetched directly from SmartOS, not meta-data values
 
62
#    base64_keys: meta-data keys that are delivered in base64
 
63
#    base64_all: with the exclusion of no_base64_decode values,
 
64
#            treat all meta-data as base64 encoded
 
65
#    disk_setup: describes how to partition the ephemeral drive
 
66
#    fs_setup: describes how to format the ephemeral drive
 
67
#
 
68
BUILTIN_DS_CONFIG = {
 
69
    'serial_device': '/dev/ttyS1',
 
70
    'seed_timeout': 60,
 
71
    'no_base64_decode': ['root_authorized_keys',
 
72
                         'motd_sys_info',
 
73
                         'iptables_disable'],
 
74
    'base64_keys': [],
 
75
    'base64_all': False,
 
76
    'disk_aliases': {'ephemeral0': '/dev/vdb'},
 
77
}
 
78
 
 
79
BUILTIN_CLOUD_CONFIG = {
 
80
    'disk_setup': {
 
81
        'ephemeral0': {'table_type': 'mbr',
 
82
                       'layout': False,
 
83
                       'overwrite': False}
 
84
         },
 
85
    'fs_setup': [{'label': 'ephemeral0',
 
86
                  'filesystem': 'ext3',
 
87
                  'device': 'ephemeral0'}],
 
88
}
 
89
 
 
90
 
 
91
class DataSourceSmartOS(sources.DataSource):
 
92
    def __init__(self, sys_cfg, distro, paths):
 
93
        sources.DataSource.__init__(self, sys_cfg, distro, paths)
 
94
        self.is_smartdc = None
 
95
 
 
96
        self.ds_cfg = util.mergemanydict([
 
97
            self.ds_cfg,
 
98
            util.get_cfg_by_path(sys_cfg, DS_CFG_PATH, {}),
 
99
            BUILTIN_DS_CONFIG])
 
100
 
 
101
        self.metadata = {}
 
102
        self.cfg = BUILTIN_CLOUD_CONFIG
 
103
 
 
104
        self.seed = self.ds_cfg.get("serial_device")
 
105
        self.seed_timeout = self.ds_cfg.get("serial_timeout")
 
106
        self.smartos_no_base64 = self.ds_cfg.get('no_base64_decode')
 
107
        self.b64_keys = self.ds_cfg.get('base64_keys')
 
108
        self.b64_all = self.ds_cfg.get('base64_all')
 
109
 
 
110
    def __str__(self):
 
111
        root = sources.DataSource.__str__(self)
 
112
        return "%s [seed=%s]" % (root, self.seed)
 
113
 
 
114
    def get_data(self):
 
115
        md = {}
 
116
        ud = ""
 
117
 
 
118
        if not os.path.exists(self.seed):
 
119
            LOG.debug("Host does not appear to be on SmartOS")
 
120
            return False
 
121
 
 
122
        dmi_info = dmi_data()
 
123
        if dmi_info is False:
 
124
            LOG.debug("No dmidata utility found")
 
125
            return False
 
126
 
 
127
        system_uuid, system_type = dmi_info
 
128
        if 'smartdc' not in system_type.lower():
 
129
            LOG.debug("Host is not on SmartOS. system_type=%s", system_type)
 
130
            return False
 
131
        self.is_smartdc = True
 
132
        md['instance-id'] = system_uuid
 
133
 
 
134
        b64_keys = self.query('base64_keys', strip=True, b64=False)
 
135
        if b64_keys is not None:
 
136
            self.b64_keys = [k.strip() for k in str(b64_keys).split(',')]
 
137
 
 
138
        b64_all = self.query('base64_all', strip=True, b64=False)
 
139
        if b64_all is not None:
 
140
            self.b64_all = util.is_true(b64_all)
 
141
 
 
142
        for ci_noun, attribute in SMARTOS_ATTRIB_MAP.iteritems():
 
143
            smartos_noun, strip = attribute
 
144
            md[ci_noun] = self.query(smartos_noun, strip=strip)
 
145
 
 
146
        if not md['local-hostname']:
 
147
            md['local-hostname'] = system_uuid
 
148
 
 
149
        ud = None
 
150
        if md['user-data']:
 
151
            ud = md['user-data']
 
152
        elif md['user-script']:
 
153
            ud = md['user-script']
 
154
 
 
155
        self.metadata = util.mergemanydict([md, self.metadata])
 
156
        self.userdata_raw = ud
 
157
        return True
 
158
 
 
159
    def device_name_to_device(self, name):
 
160
        return self.ds_cfg['disk_aliases'].get(name)
 
161
 
 
162
    def get_config_obj(self):
 
163
        return self.cfg
 
164
 
 
165
    def get_instance_id(self):
 
166
        return self.metadata['instance-id']
 
167
 
 
168
    def query(self, noun, strip=False, default=None, b64=None):
 
169
        if b64 is None:
 
170
            if noun in self.smartos_no_base64:
 
171
                b64 = False
 
172
            elif self.b64_all or noun in self.b64_keys:
 
173
                b64 = True
 
174
 
 
175
        return query_data(noun=noun, strip=strip, seed_device=self.seed,
 
176
                          seed_timeout=self.seed_timeout, default=default,
 
177
                          b64=b64)
 
178
 
 
179
 
 
180
def get_serial(seed_device, seed_timeout):
 
181
    """This is replaced in unit testing, allowing us to replace
 
182
        serial.Serial with a mocked class.
 
183
 
 
184
        The timeout value of 60 seconds should never be hit. The value
 
185
        is taken from SmartOS own provisioning tools. Since we are reading
 
186
        each line individually up until the single ".", the transfer is
 
187
        usually very fast (i.e. microseconds) to get the response.
 
188
    """
 
189
    if not seed_device:
 
190
        raise AttributeError("seed_device value is not set")
 
191
 
 
192
    ser = serial.Serial(seed_device, timeout=seed_timeout)
 
193
    if not ser.isOpen():
 
194
        raise SystemError("Unable to open %s" % seed_device)
 
195
 
 
196
    return ser
 
197
 
 
198
 
 
199
def query_data(noun, seed_device, seed_timeout, strip=False, default=None,
 
200
               b64=None):
 
201
    """Makes a request to via the serial console via "GET <NOUN>"
 
202
 
 
203
        In the response, the first line is the status, while subsequent lines
 
204
        are is the value. A blank line with a "." is used to indicate end of
 
205
        response.
 
206
 
 
207
        If the response is expected to be base64 encoded, then set b64encoded
 
208
        to true. Unfortantely, there is no way to know if something is 100%
 
209
        encoded, so this method relies on being told if the data is base64 or
 
210
        not.
 
211
    """
 
212
 
 
213
    if not noun:
 
214
        return False
 
215
 
 
216
    ser = get_serial(seed_device, seed_timeout)
 
217
    ser.write("GET %s\n" % noun.rstrip())
 
218
    status = str(ser.readline()).rstrip()
 
219
    response = []
 
220
    eom_found = False
 
221
 
 
222
    if 'SUCCESS' not in status:
 
223
        ser.close()
 
224
        return default
 
225
 
 
226
    while not eom_found:
 
227
        m = ser.readline()
 
228
        if m.rstrip() == ".":
 
229
            eom_found = True
 
230
        else:
 
231
            response.append(m)
 
232
 
 
233
    ser.close()
 
234
 
 
235
    if b64 is None:
 
236
        b64 = query_data('b64-%s' % noun, seed_device=seed_device,
 
237
                            seed_timeout=seed_timeout, b64=False,
 
238
                            default=False, strip=True)
 
239
        b64 = util.is_true(b64)
 
240
 
 
241
    resp = None
 
242
    if b64 or strip:
 
243
        resp = "".join(response).rstrip()
 
244
    else:
 
245
        resp = "".join(response)
 
246
 
 
247
    if b64:
 
248
        try:
 
249
            return base64.b64decode(resp)
 
250
        except TypeError:
 
251
            LOG.warn("Failed base64 decoding key '%s'", noun)
 
252
            return resp
 
253
 
 
254
    return resp
 
255
 
 
256
 
 
257
def dmi_data():
 
258
    sys_uuid, sys_type = None, None
 
259
    dmidecode_path = util.which('dmidecode')
 
260
    if not dmidecode_path:
 
261
        return False
 
262
 
 
263
    sys_uuid_cmd = [dmidecode_path, "-s", "system-uuid"]
 
264
    try:
 
265
        LOG.debug("Getting hostname from dmidecode")
 
266
        (sys_uuid, _err) = util.subp(sys_uuid_cmd)
 
267
    except Exception as e:
 
268
        util.logexc(LOG, "Failed to get system UUID", e)
 
269
 
 
270
    sys_type_cmd = [dmidecode_path, "-s", "system-product-name"]
 
271
    try:
 
272
        LOG.debug("Determining hypervisor product name via dmidecode")
 
273
        (sys_type, _err) = util.subp(sys_type_cmd)
 
274
    except Exception as e:
 
275
        util.logexc(LOG, "Failed to get system UUID", e)
 
276
 
 
277
    return (sys_uuid.lower().strip(), sys_type.strip())
 
278
 
 
279
 
 
280
# Used to match classes to dependencies
 
281
datasources = [
 
282
    (DataSourceSmartOS, (sources.DEP_FILESYSTEM, sources.DEP_NETWORK)),
 
283
]
 
284
 
 
285
 
 
286
# Return a list of data sources that match this set of dependencies
 
287
def get_datasource_list(depends):
 
288
    return sources.list_from_depends(depends, datasources)