1# Licensed under the Apache License, Version 2.0 (the "License"); you may
2# not use this file except in compliance with the License. You may obtain
3# a copy of the License at
4#
5#      http://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10# License for the specific language governing permissions and limitations
11# under the License.
12r"""
13Remote File
14-----------
15
16The **remote_file** backend driver is the first driver implemented by
17oslo.config. It extends the previous limit of only accessing local files
18to a new scenario where it is possible to access configuration data over
19the network. The **remote_file** driver is based on the **requests** module
20and is capable of accessing remote files through **HTTP** or **HTTPS**.
21
22To definition of a remote_file configuration data source can be as minimal as::
23
24    [DEFAULT]
25    config_source = external_config_group
26
27    [external_config_group]
28    driver = remote_file
29    uri = http://mydomain.com/path/to/config/data.conf
30
31Or as complete as::
32
33    [DEFAULT]
34    config_source = external_config_group
35
36    [external_config_group]
37    driver = remote_file
38    uri = https://mydomain.com/path/to/config/data.conf
39    ca_path = /path/to/server/ca.pem
40    client_key = /path/to/my/key.pem
41    client_cert = /path/to/my/cert.pem
42
43On the following sessions, you can find more information about this driver's
44classes and its options.
45
46The Driver Class
47================
48
49.. autoclass:: URIConfigurationSourceDriver
50
51The Configuration Source Class
52==============================
53
54.. autoclass:: URIConfigurationSource
55
56"""
57
58import requests
59import tempfile
60
61from oslo_config import cfg
62from oslo_config import sources
63
64
65class URIConfigurationSourceDriver(sources.ConfigurationSourceDriver):
66    """A backend driver for remote files served through http[s].
67
68    Required options:
69      - uri: URI containing the file location.
70
71    Non-required options:
72      - ca_path: The path to a CA_BUNDLE file or directory with
73                 certificates of trusted CAs.
74
75      - client_cert: Client side certificate, as a single file path
76                     containing either the certificate only or the
77                     private key and the certificate.
78
79      - client_key: Client side private key, in case client_cert is
80                    specified but does not includes the private key.
81    """
82
83    _uri_driver_opts = [
84        cfg.URIOpt(
85            'uri',
86            schemes=['http', 'https'],
87            required=True,
88            sample_default='https://example.com/my-configuration.ini',
89            help=('Required option with the URI of the '
90                  'extra configuration file\'s location.'),
91        ),
92        cfg.StrOpt(
93            'ca_path',
94            sample_default='/usr/local/etc/ca-certificates',
95            help=('The path to a CA_BUNDLE file or directory '
96                  'with certificates of trusted CAs.'),
97        ),
98        cfg.StrOpt(
99            'client_cert',
100            sample_default='/usr/local/etc/ca-certificates/service-client-keystore',
101            help=('Client side certificate, as a single file path '
102                  'containing either the certificate only or the '
103                  'private key and the certificate.'),
104        ),
105        cfg.StrOpt(
106            'client_key',
107            help=('Client side private key, in case client_cert is '
108                  'specified but does not includes the private key.'),
109        ),
110    ]
111
112    def list_options_for_discovery(self):
113        return self._uri_driver_opts
114
115    def open_source_from_opt_group(self, conf, group_name):
116        conf.register_opts(self._uri_driver_opts, group_name)
117
118        return URIConfigurationSource(
119            conf[group_name].uri,
120            conf[group_name].ca_path,
121            conf[group_name].client_cert,
122            conf[group_name].client_key)
123
124
125class URIConfigurationSource(sources.ConfigurationSource):
126    """A configuration source for remote files served through http[s].
127
128    :param uri: The Uniform Resource Identifier of the configuration to be
129          retrieved.
130
131    :param ca_path: The path to a CA_BUNDLE file or directory with
132              certificates of trusted CAs.
133
134    :param client_cert: Client side certificate, as a single file path
135                  containing either the certificate only or the
136                  private key and the certificate.
137
138    :param client_key: Client side private key, in case client_cert is
139                 specified but does not includes the private key.
140    """
141
142    def __init__(self, uri, ca_path=None, client_cert=None, client_key=None):
143        self._uri = uri
144        self._namespace = cfg._Namespace(cfg.ConfigOpts())
145
146        data = self._fetch_uri(uri, ca_path, client_cert, client_key)
147
148        with tempfile.NamedTemporaryFile() as tmpfile:
149            tmpfile.write(data.encode("utf-8"))
150            tmpfile.flush()
151
152            cfg.ConfigParser._parse_file(tmpfile.name, self._namespace)
153
154    def _fetch_uri(self, uri, ca_path, client_cert, client_key):
155        verify = ca_path if ca_path else True
156        cert = (client_cert, client_key) if client_cert and client_key else \
157            client_cert
158
159        with requests.get(uri, verify=verify, cert=cert) as response:
160            response.raise_for_status()  # raises only in case of HTTPError
161
162            return response.text
163
164    def get(self, group_name, option_name, opt):
165        try:
166            return self._namespace._get_value(
167                [(group_name, option_name)],
168                multi=opt.multi)
169        except KeyError:
170            return (sources._NoValue, None)
171