This file is indexed.

/usr/lib/python2.7/dist-packages/provisioningserver/omshell.py is in python-maas-provisioningserver 1.5.4+bzr2294-0ubuntu1.2.

This file is owned by root:root, with mode 0o644.

The actual contents of the file can be viewed below.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# Copyright 2012 Canonical Ltd.  This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).

"""Python wrapper around the `omshell` utility which amends objects
inside the DHCP server.
"""

from __future__ import (
    absolute_import,
    print_function,
    unicode_literals,
    )

str = None

__metaclass__ = type
__all__ = [
    "generate_omapi_key",
    "Omshell",
    ]

import os
import re
from subprocess import (
    PIPE,
    Popen,
    )
from textwrap import dedent

from provisioningserver.utils import (
    call_capture_and_check,
    ExternalProcessError,
    parse_key_value_file,
    tempdir,
    )


bad_key_pattern = re.compile("[+/]no|no[+/]", flags=re.IGNORECASE)


def call_dnssec_keygen(tmpdir):
    path = os.environ.get("PATH", "").split(os.pathsep)
    path.append("/usr/sbin")
    env = dict(os.environ, PATH=os.pathsep.join(path))
    return call_capture_and_check(
        ['dnssec-keygen', '-r', '/dev/urandom', '-a', 'HMAC-MD5',
         '-b', '512', '-n', 'HOST', '-K', tmpdir, '-q', 'omapi_key'],
        env=env)


def run_repeated_keygen(tmpdir):
    # omshell has a bug where if the chars '/' or '+' appear either
    # side of the word 'no' (in any case), it throws an error like
    # "partial base64 value left over".  We check for that here and
    # repeatedly generate a new key until a good one is generated.

    key = None
    while key is None:
        key_id = call_dnssec_keygen(tmpdir)

        # Locate the file that was written and strip out the Key: field in
        # it.
        if not key_id:
            raise AssertionError("dnssec-keygen didn't generate anything")
        key_id = key_id.strip()  # Remove trailing newline.
        key_file_name = os.path.join(tmpdir, key_id + '.private')
        parsing_error = False
        try:
            config = parse_key_value_file(key_file_name)
        except ValueError:
            parsing_error = True
        if parsing_error or 'Key' not in config:
            raise AssertionError(
                "Key field not found in output from dnssec-keygen")

        key = config['Key']
        if bad_key_pattern.search(key) is not None:
            # Force a retry.
            os.remove(key_file_name)  # Stop dnssec_keygen complaints.
            key = None

    return key


def generate_omapi_key():
    """Generate a HMAC-MD5 key by calling out to the dnssec-keygen tool.

    :return: The shared key suitable for OMAPI access.
    :type: string
    """
    # dnssec-keygen writes out files to a specified directory, so we
    # need to make a temp directory for that.
    # This relies on the temporary directory being accessible only to its
    # owner.
    temp_prefix = "%s." % os.path.basename(__file__)
    with tempdir(prefix=temp_prefix) as tmpdir:
        key = run_repeated_keygen(tmpdir)
        return key


class Omshell:
    """Wrap up the omshell utility in Python.

    'omshell' is an external executable that communicates with a DHCP daemon
    and manipulates its objects.  This class wraps up the commands necessary
    to add and remove host maps (MAC to IP).

    :param server_address: The address for the DHCP server (ip or hostname)
    :param shared_key: An HMAC-MD5 key generated by dnssec-keygen like:
        $ dnssec-keygen -r /dev/urandom -a HMAC-MD5 -b 512 -n HOST omapi_key
        $ cat Komapi_key.+*.private |grep ^Key|cut -d ' ' -f2-
        It must match the key set in the DHCP server's config which looks
        like this:

        omapi-port 7911;
        key omapi_key {
            algorithm HMAC-MD5;
            secret "XXXXXXXXX"; #<-The output from the generated key above.
        };
        omapi-key omapi_key;
    """

    def __init__(self, server_address, shared_key):
        self.server_address = server_address
        self.shared_key = shared_key
        self.command = ["omshell"]

    def _run(self, stdin):
        proc = Popen(self.command, stdin=PIPE, stdout=PIPE)
        stdout, stderr = proc.communicate(stdin)
        if proc.poll() != 0:
            raise ExternalProcessError(proc.returncode, self.command, stdout)
        return proc.returncode, stdout

    def create(self, ip_address, mac_address):
        # The "name" is not a host name; it's an identifier used within
        # the DHCP server.  We just happen to use the IP address.
        stdin = dedent("""\
            server {self.server_address}
            key omapi_key {self.shared_key}
            connect
            new host
            set ip-address = {ip_address}
            set hardware-address = {mac_address}
            set hardware-type = 1
            set name = "{ip_address}"
            create
            """)
        stdin = stdin.format(
            self=self, ip_address=ip_address, mac_address=mac_address)

        returncode, output = self._run(stdin)
        # If the call to omshell doesn't result in output containing the
        # magic string 'hardware-type' then we can be reasonably sure
        # that the 'create' command failed.  Unfortunately there's no
        # other output like "successful" to check so this is the best we
        # can do.
        if "hardware-type" in output:
            # Success.
            pass
        elif "can't open object: I/O error" in output:
            # Host map already existed.  Treat as success.
            pass
        else:
            raise ExternalProcessError(returncode, self.command, output)

    def remove(self, ip_address):
        # The "name" is not a host name; it's an identifier used within
        # the DHCP server.  We just happen to use the IP address.
        stdin = dedent("""\
            server {self.server_address}
            key omapi_key {self.shared_key}
            connect
            new host
            set name = "{ip_address}"
            open
            remove
            """)
        stdin = stdin.format(
            self=self, ip_address=ip_address)

        returncode, output = self._run(stdin)

        # If the omshell worked, the last line should reference a null
        # object.
        lines = output.strip().splitlines()
        try:
            last_line = lines[-1]
        except IndexError:
            last_line = ""
        if last_line != "obj: <null>":
            raise ExternalProcessError(returncode, self.command, output)