1# Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License"). You
4# may not use this file except in compliance with the License. A copy of
5# the License is located at
6#
7# https://aws.amazon.com/apache2.0/
8#
9# or in the "license" file accompanying this file. This file is
10# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11# ANY KIND, either express or implied. See the License for the specific
12# language governing permissions and limitations under the License.
13
14import copy
15import os
16
17import botocore.session
18from botocore.client import Config
19from botocore.exceptions import DataNotFoundError, UnknownServiceError
20
21import boto3
22import boto3.utils
23from boto3.exceptions import ResourceNotExistsError, UnknownAPIVersionError
24
25from .resources.factory import ResourceFactory
26
27
28class Session(object):
29    """
30    A session stores configuration state and allows you to create service
31    clients and resources.
32
33    :type aws_access_key_id: string
34    :param aws_access_key_id: AWS access key ID
35    :type aws_secret_access_key: string
36    :param aws_secret_access_key: AWS secret access key
37    :type aws_session_token: string
38    :param aws_session_token: AWS temporary session token
39    :type region_name: string
40    :param region_name: Default region when creating new connections
41    :type botocore_session: botocore.session.Session
42    :param botocore_session: Use this Botocore session instead of creating
43                             a new default one.
44    :type profile_name: string
45    :param profile_name: The name of a profile to use. If not given, then
46                         the default profile is used.
47    """
48    def __init__(self, aws_access_key_id=None, aws_secret_access_key=None,
49                 aws_session_token=None, region_name=None,
50                 botocore_session=None, profile_name=None):
51        if botocore_session is not None:
52            self._session = botocore_session
53        else:
54            # Create a new default session
55            self._session = botocore.session.get_session()
56
57        # Setup custom user-agent string if it isn't already customized
58        if self._session.user_agent_name == 'Botocore':
59            botocore_info = 'Botocore/{0}'.format(
60                self._session.user_agent_version)
61            if self._session.user_agent_extra:
62                self._session.user_agent_extra += ' ' + botocore_info
63            else:
64                self._session.user_agent_extra = botocore_info
65            self._session.user_agent_name = 'Boto3'
66            self._session.user_agent_version = boto3.__version__
67
68        if profile_name is not None:
69            self._session.set_config_variable('profile', profile_name)
70
71        if aws_access_key_id or aws_secret_access_key or aws_session_token:
72            self._session.set_credentials(
73                aws_access_key_id, aws_secret_access_key, aws_session_token)
74
75        if region_name is not None:
76            self._session.set_config_variable('region', region_name)
77
78        self.resource_factory = ResourceFactory(
79            self._session.get_component('event_emitter'))
80        self._setup_loader()
81        self._register_default_handlers()
82
83    def __repr__(self):
84        return '{0}(region_name={1})'.format(
85            self.__class__.__name__,
86            repr(self._session.get_config_variable('region')))
87
88    @property
89    def profile_name(self):
90        """
91        The **read-only** profile name.
92        """
93        return self._session.profile or 'default'
94
95    @property
96    def region_name(self):
97        """
98        The **read-only** region name.
99        """
100        return self._session.get_config_variable('region')
101
102    @property
103    def events(self):
104        """
105        The event emitter for a session
106        """
107        return self._session.get_component('event_emitter')
108
109    @property
110    def available_profiles(self):
111        """
112        The profiles available to the session credentials
113        """
114        return self._session.available_profiles
115
116    def _setup_loader(self):
117        """
118        Setup loader paths so that we can load resources.
119        """
120        self._loader = self._session.get_component('data_loader')
121        self._loader.search_paths.append(
122            os.path.join(os.path.dirname(__file__), 'data'))
123
124    def get_available_services(self):
125        """
126        Get a list of available services that can be loaded as low-level
127        clients via :py:meth:`Session.client`.
128
129        :rtype: list
130        :return: List of service names
131        """
132        return self._session.get_available_services()
133
134    def get_available_resources(self):
135        """
136        Get a list of available services that can be loaded as resource
137        clients via :py:meth:`Session.resource`.
138
139        :rtype: list
140        :return: List of service names
141        """
142        return self._loader.list_available_services(type_name='resources-1')
143
144    def get_available_partitions(self):
145        """Lists the available partitions
146
147        :rtype: list
148        :return: Returns a list of partition names (e.g., ["aws", "aws-cn"])
149        """
150        return self._session.get_available_partitions()
151
152    def get_available_regions(self, service_name, partition_name='aws',
153                              allow_non_regional=False):
154        """Lists the region and endpoint names of a particular partition.
155
156        :type service_name: string
157        :param service_name: Name of a service to list endpoint for (e.g., s3).
158
159        :type partition_name: string
160        :param partition_name: Name of the partition to limit endpoints to.
161            (e.g., aws for the public AWS endpoints, aws-cn for AWS China
162            endpoints, aws-us-gov for AWS GovCloud (US) Endpoints, etc.)
163
164        :type allow_non_regional: bool
165        :param allow_non_regional: Set to True to include endpoints that are
166             not regional endpoints (e.g., s3-external-1,
167             fips-us-gov-west-1, etc).
168
169        :return: Returns a list of endpoint names (e.g., ["us-east-1"]).
170        """
171        return self._session.get_available_regions(
172            service_name=service_name, partition_name=partition_name,
173            allow_non_regional=allow_non_regional)
174
175    def get_credentials(self):
176        """
177        Return the :class:`botocore.credential.Credential` object
178        associated with this session.  If the credentials have not
179        yet been loaded, this will attempt to load them.  If they
180        have already been loaded, this will return the cached
181        credentials.
182        """
183        return self._session.get_credentials()
184
185    def client(self, service_name, region_name=None, api_version=None,
186               use_ssl=True, verify=None, endpoint_url=None,
187               aws_access_key_id=None, aws_secret_access_key=None,
188               aws_session_token=None, config=None):
189        """
190        Create a low-level service client by name.
191
192        :type service_name: string
193        :param service_name: The name of a service, e.g. 's3' or 'ec2'. You
194            can get a list of available services via
195            :py:meth:`get_available_services`.
196
197        :type region_name: string
198        :param region_name: The name of the region associated with the client.
199            A client is associated with a single region.
200
201        :type api_version: string
202        :param api_version: The API version to use.  By default, botocore will
203            use the latest API version when creating a client.  You only need
204            to specify this parameter if you want to use a previous API version
205            of the client.
206
207        :type use_ssl: boolean
208        :param use_ssl: Whether or not to use SSL.  By default, SSL is used.
209            Note that not all services support non-ssl connections.
210
211        :type verify: boolean/string
212        :param verify: Whether or not to verify SSL certificates.  By default
213            SSL certificates are verified.  You can provide the following
214            values:
215
216            * False - do not validate SSL certificates.  SSL will still be
217              used (unless use_ssl is False), but SSL certificates
218              will not be verified.
219            * path/to/cert/bundle.pem - A filename of the CA cert bundle to
220              uses.  You can specify this argument if you want to use a
221              different CA cert bundle than the one used by botocore.
222
223        :type endpoint_url: string
224        :param endpoint_url: The complete URL to use for the constructed
225            client. Normally, botocore will automatically construct the
226            appropriate URL to use when communicating with a service.  You
227            can specify a complete URL (including the "http/https" scheme)
228            to override this behavior.  If this value is provided,
229            then ``use_ssl`` is ignored.
230
231        :type aws_access_key_id: string
232        :param aws_access_key_id: The access key to use when creating
233            the client.  This is entirely optional, and if not provided,
234            the credentials configured for the session will automatically
235            be used.  You only need to provide this argument if you want
236            to override the credentials used for this specific client.
237
238        :type aws_secret_access_key: string
239        :param aws_secret_access_key: The secret key to use when creating
240            the client.  Same semantics as aws_access_key_id above.
241
242        :type aws_session_token: string
243        :param aws_session_token: The session token to use when creating
244            the client.  Same semantics as aws_access_key_id above.
245
246        :type config: botocore.client.Config
247        :param config: Advanced client configuration options. If region_name
248            is specified in the client config, its value will take precedence
249            over environment variables and configuration values, but not over
250            a region_name value passed explicitly to the method. See
251            `botocore config documentation
252            <https://botocore.amazonaws.com/v1/documentation/api/latest/reference/config.html>`_
253            for more details.
254
255        :return: Service client instance
256
257        """
258        return self._session.create_client(
259            service_name, region_name=region_name, api_version=api_version,
260            use_ssl=use_ssl, verify=verify, endpoint_url=endpoint_url,
261            aws_access_key_id=aws_access_key_id,
262            aws_secret_access_key=aws_secret_access_key,
263            aws_session_token=aws_session_token, config=config)
264
265    def resource(self, service_name, region_name=None, api_version=None,
266                 use_ssl=True, verify=None, endpoint_url=None,
267                 aws_access_key_id=None, aws_secret_access_key=None,
268                 aws_session_token=None, config=None):
269        """
270        Create a resource service client by name.
271
272        :type service_name: string
273        :param service_name: The name of a service, e.g. 's3' or 'ec2'. You
274            can get a list of available services via
275            :py:meth:`get_available_resources`.
276
277        :type region_name: string
278        :param region_name: The name of the region associated with the client.
279            A client is associated with a single region.
280
281        :type api_version: string
282        :param api_version: The API version to use.  By default, botocore will
283            use the latest API version when creating a client.  You only need
284            to specify this parameter if you want to use a previous API version
285            of the client.
286
287        :type use_ssl: boolean
288        :param use_ssl: Whether or not to use SSL.  By default, SSL is used.
289            Note that not all services support non-ssl connections.
290
291        :type verify: boolean/string
292        :param verify: Whether or not to verify SSL certificates.  By default
293            SSL certificates are verified.  You can provide the following
294            values:
295
296            * False - do not validate SSL certificates.  SSL will still be
297              used (unless use_ssl is False), but SSL certificates
298              will not be verified.
299            * path/to/cert/bundle.pem - A filename of the CA cert bundle to
300              uses.  You can specify this argument if you want to use a
301              different CA cert bundle than the one used by botocore.
302
303        :type endpoint_url: string
304        :param endpoint_url: The complete URL to use for the constructed
305            client. Normally, botocore will automatically construct the
306            appropriate URL to use when communicating with a service.  You
307            can specify a complete URL (including the "http/https" scheme)
308            to override this behavior.  If this value is provided,
309            then ``use_ssl`` is ignored.
310
311        :type aws_access_key_id: string
312        :param aws_access_key_id: The access key to use when creating
313            the client.  This is entirely optional, and if not provided,
314            the credentials configured for the session will automatically
315            be used.  You only need to provide this argument if you want
316            to override the credentials used for this specific client.
317
318        :type aws_secret_access_key: string
319        :param aws_secret_access_key: The secret key to use when creating
320            the client.  Same semantics as aws_access_key_id above.
321
322        :type aws_session_token: string
323        :param aws_session_token: The session token to use when creating
324            the client.  Same semantics as aws_access_key_id above.
325
326        :type config: botocore.client.Config
327        :param config: Advanced client configuration options. If region_name
328            is specified in the client config, its value will take precedence
329            over environment variables and configuration values, but not over
330            a region_name value passed explicitly to the method.  If
331            user_agent_extra is specified in the client config, it overrides
332            the default user_agent_extra provided by the resource API. See
333            `botocore config documentation
334            <https://botocore.amazonaws.com/v1/documentation/api/latest/reference/config.html>`_
335            for more details.
336
337        :return: Subclass of :py:class:`~boto3.resources.base.ServiceResource`
338        """
339        try:
340            resource_model = self._loader.load_service_model(
341                service_name, 'resources-1', api_version)
342        except UnknownServiceError:
343            available = self.get_available_resources()
344            has_low_level_client = (
345                service_name in self.get_available_services())
346            raise ResourceNotExistsError(service_name, available,
347                                         has_low_level_client)
348        except DataNotFoundError:
349            # This is because we've provided an invalid API version.
350            available_api_versions = self._loader.list_api_versions(
351                service_name, 'resources-1')
352            raise UnknownAPIVersionError(
353                service_name, api_version, ', '.join(available_api_versions))
354
355        if api_version is None:
356            # Even though botocore's load_service_model() can handle
357            # using the latest api_version if not provided, we need
358            # to track this api_version in boto3 in order to ensure
359            # we're pairing a resource model with a client model
360            # of the same API version.  It's possible for the latest
361            # API version of a resource model in boto3 to not be
362            # the same API version as a service model in botocore.
363            # So we need to look up the api_version if one is not
364            # provided to ensure we load the same API version of the
365            # client.
366            #
367            # Note: This is relying on the fact that
368            #   loader.load_service_model(..., api_version=None)
369            # and loader.determine_latest_version(..., 'resources-1')
370            # both load the same api version of the file.
371            api_version = self._loader.determine_latest_version(
372                service_name, 'resources-1')
373
374        # Creating a new resource instance requires the low-level client
375        # and service model, the resource version and resource JSON data.
376        # We pass these to the factory and get back a class, which is
377        # instantiated on top of the low-level client.
378        if config is not None:
379            if config.user_agent_extra is None:
380                config = copy.deepcopy(config)
381                config.user_agent_extra = 'Resource'
382        else:
383            config = Config(user_agent_extra='Resource')
384        client = self.client(
385            service_name, region_name=region_name, api_version=api_version,
386            use_ssl=use_ssl, verify=verify, endpoint_url=endpoint_url,
387            aws_access_key_id=aws_access_key_id,
388            aws_secret_access_key=aws_secret_access_key,
389            aws_session_token=aws_session_token, config=config)
390        service_model = client.meta.service_model
391
392        # Create a ServiceContext object to serve as a reference to
393        # important read-only information about the general service.
394        service_context = boto3.utils.ServiceContext(
395            service_name=service_name, service_model=service_model,
396            resource_json_definitions=resource_model['resources'],
397            service_waiter_model=boto3.utils.LazyLoadedWaiterModel(
398                self._session, service_name, api_version
399            )
400        )
401
402        # Create the service resource class.
403        cls = self.resource_factory.load_from_definition(
404            resource_name=service_name,
405            single_resource_json_definition=resource_model['service'],
406            service_context=service_context
407        )
408
409        return cls(client=client)
410
411    def _register_default_handlers(self):
412
413        # S3 customizations
414        self._session.register(
415            'creating-client-class.s3',
416            boto3.utils.lazy_call(
417                'boto3.s3.inject.inject_s3_transfer_methods'))
418        self._session.register(
419            'creating-resource-class.s3.Bucket',
420            boto3.utils.lazy_call(
421                'boto3.s3.inject.inject_bucket_methods'))
422        self._session.register(
423            'creating-resource-class.s3.Object',
424            boto3.utils.lazy_call(
425                'boto3.s3.inject.inject_object_methods'))
426        self._session.register(
427            'creating-resource-class.s3.ObjectSummary',
428            boto3.utils.lazy_call(
429                'boto3.s3.inject.inject_object_summary_methods'))
430
431        # DynamoDb customizations
432        self._session.register(
433            'creating-resource-class.dynamodb',
434            boto3.utils.lazy_call(
435                'boto3.dynamodb.transform.register_high_level_interface'),
436            unique_id='high-level-dynamodb')
437        self._session.register(
438            'creating-resource-class.dynamodb.Table',
439            boto3.utils.lazy_call(
440                'boto3.dynamodb.table.register_table_methods'),
441            unique_id='high-level-dynamodb-table')
442
443        # EC2 Customizations
444        self._session.register(
445            'creating-resource-class.ec2.ServiceResource',
446            boto3.utils.lazy_call(
447                'boto3.ec2.createtags.inject_create_tags'))
448
449        self._session.register(
450            'creating-resource-class.ec2.Instance',
451            boto3.utils.lazy_call(
452                'boto3.ec2.deletetags.inject_delete_tags',
453                event_emitter=self.events))
454