Make dcae-cli more tolerant aka configurable
[dcaegen2/platform/cli.git] / dcae-cli / dcae_cli / util / profiles.py
1 # ============LICENSE_START=======================================================
2 # org.onap.dcae
3 # ================================================================================
4 # Copyright (c) 2017 AT&T Intellectual Property. All rights reserved.
5 # ================================================================================
6 # Licensed under the Apache License, Version 2.0 (the "License");
7 # you may not use this file except in compliance with the License.
8 # You may obtain a copy of the License at
9 #
10 #      http://www.apache.org/licenses/LICENSE-2.0
11 #
12 # Unless required by applicable law or agreed to in writing, software
13 # distributed under the License is distributed on an "AS IS" BASIS,
14 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 # See the License for the specific language governing permissions and
16 # limitations under the License.
17 # ============LICENSE_END=========================================================
18 #
19 # ECOMP is a trademark and service mark of AT&T Intellectual Property.
20
21 #!/usr/bin/env python3
22 # -*- coding: utf-8 -*-
23 """
24 Provides dcae cli profile variables
25 """
26 import os
27 from collections import namedtuple
28
29 import six
30 import click
31
32 from dcae_cli import util
33 from dcae_cli.util import get_app_dir, get_pref, write_pref
34 from dcae_cli.util import config
35 from dcae_cli.util.config import get_config, update_config
36 from dcae_cli.util.exc import DcaeException
37 from dcae_cli.util.logger import get_logger
38
39
40 logger = get_logger('Profile')
41
42
43 # reserved profile names
44 ACTIVE = 'active'
45 _reserved_names = {ACTIVE}
46
47
48 # create enums for profile keys so that they can be imported for testing, instead of using literals
49 CONSUL_HOST = 'consul_host'
50 CONFIG_BINDING_SERVICE = 'config_binding_service'
51 CDAP_BROKER = 'cdap_broker'
52 DOCKER_HOST = 'docker_host'
53
54 # TODO: Should probably lift this strict list of allowed keys and repurpose to be
55 # keys that are required.
56 _allowed_keys = set([CONSUL_HOST, CONFIG_BINDING_SERVICE, CDAP_BROKER, DOCKER_HOST])
57 Profile = namedtuple('Profile', _allowed_keys)
58
59
60 def _create_stub_profile():
61     """Create a new stub of a profile"""
62     return { k: "" for k in _allowed_keys }
63
64
65 def _fmt_seq(seq):
66     '''Returns a sorted string formatted list'''
67     return list(sorted(map(str, seq)))
68
69
70 def get_profiles_path():
71     '''Returns the absolute path to the profiles file'''
72     return os.path.join(get_app_dir(), 'profiles.json')
73
74
75 def get_active_name():
76     '''Returns the active profile name in the config'''
77     return config.get_active_profile()
78
79
80 def _set_active_name(name):
81     '''Sets the active profile name in the config'''
82     update_config(active_profile=name)
83
84
85 class ProfilesInitError(RuntimeError):
86     pass
87
88 def reinit_profiles():
89     """Reinitialize profiles
90
91     Grab the remote profiles and merge with the local profiles if there is one.
92
93     Returns:
94     --------
95     Dict of complete new profiles
96     """
97     # Grab the remote profiles and merge it in
98     try:
99         server_url = config.get_server_url()
100         new_profiles = util.fetch_file_from_web(server_url, "/dcae-cli/profiles.json")
101     except:
102         # Failing to pull seed profiles from remote server is not considered
103         # a problem. Just continue and give user the option to use an empty
104         # default.
105         if click.confirm("Could not download initial profiles from remote server. Set empty default?"):
106             new_profiles = {"default": { "consul_host": "", "config_binding_service": "", 
107                 "cdap_broker": "", "docker_host": ""}}
108         else:
109             raise ProfilesInitError("Could not setup dcae-cli profiles")
110
111     profiles_path = get_profiles_path()
112
113     if  util.pref_exists(profiles_path):
114         existing_profiles = get_profiles(include_active=False)
115         # Make sure to clobber existing values and not other way
116         existing_profiles.update(new_profiles)
117         new_profiles = existing_profiles
118
119     write_pref(new_profiles, profiles_path)
120     return new_profiles
121
122
123 def get_profiles(user_only=False, include_active=True):
124     '''Returns a dict containing all available profiles
125
126     Example of the returned dict:
127         {
128             "profile-foo": {
129                 "some_variable_A": "some_value_A",
130                 "some_variable_B": "some_value_B",
131                 "some_variable_C": "some_value_C"
132             }
133         }
134     '''
135     try:
136         profiles = get_pref(get_profiles_path(), reinit_profiles)
137     except ProfilesInitError as e:
138         raise DcaeException("Failed to initialize profiles: {0}".format(e))
139
140     if user_only:
141         return profiles
142
143     if include_active:
144         active_name = get_active_name()
145         if active_name not in profiles:
146             raise DcaeException("Active profile '{}' does not exist. How did this happen?".format(active_name))
147         profiles[ACTIVE] = profiles[active_name]
148
149     return profiles
150
151
152 def get_profile(name=ACTIVE):
153     '''Returns a `Profile` object'''
154     profiles = get_profiles()
155
156     if name not in profiles:
157         raise DcaeException("Specified profile '{}' does not exist.".format(name))
158
159     try:
160         profile = Profile(**profiles[name])
161     except TypeError as e:
162         raise DcaeException("Specified profile '{}' is malformed.".format(name))
163
164     return profile
165
166
167 def create_profile(name, **kwargs):
168     '''Creates a new profile'''
169     _assert_not_reserved(name)
170
171     profiles = get_profiles(user_only=True)
172     if name in profiles:
173         raise DcaeException("Profile '{}' already exists.".format(name))
174
175     profile = _create_stub_profile()
176     profile.update(kwargs)
177     _assert_valid_profile(profile)
178
179     profiles[name] = profile
180     _write_profiles(profiles)
181
182
183 def delete_profile(name):
184     '''Deletes a profile'''
185     _assert_not_reserved(name)
186     profiles = get_profiles(user_only=True)
187     if name not in profiles:
188         raise DcaeException("Profile '{}' does not exist.".format(name))
189     if name == get_active_name():
190         logger.warning("Profile '{}' is currently active. Activate another profile first."
191                 .format(name))
192         return False
193     del profiles[name]
194     _write_profiles(profiles)
195     return True
196
197
198 def update_profile(name, **kwargs):
199     '''Creates or updates a profile'''
200     _assert_not_reserved(name)
201     _assert_valid_profile(kwargs)
202
203     profiles = get_profiles(user_only=True)
204     if name not in profiles:
205         raise DcaeException("Profile '{}' does not exist.".format(name))
206
207     profiles[name].update(kwargs)
208     _write_profiles(profiles)
209
210
211 def _assert_valid_profile(params):
212     '''Raises DcaeException if the profile parameter dict is invalid'''
213     if not params:
214         raise DcaeException('No update key-value pairs were provided.')
215     keys = set(params.keys())
216     if not _allowed_keys.issuperset(keys):
217         invalid_keys = keys - _allowed_keys
218         raise DcaeException("Invalid keys {} detected. Only keys {} are supported.".format(_fmt_seq(invalid_keys), _fmt_seq(_allowed_keys)))
219
220
221 def _assert_not_reserved(name):
222     '''Raises DcaeException if the profile is reserved'''
223     if name in _reserved_names:
224         raise DcaeException("Profile '{}' is reserved and cannot be modified.".format(name))
225
226
227 def _write_profiles(profiles):
228     '''Writes the profiles dictionary to disk'''
229     return write_pref(profiles, path=get_profiles_path())
230
231
232 def activate_profile(name):
233     '''Modifies the config and sets a new active profile'''
234     avail_profiles = set(get_profiles().keys()) - {ACTIVE, }
235     if name not in avail_profiles:
236         raise DcaeException("Profile name '{}' does not exist. Please select from {} or create a new profile.".format(name, _fmt_seq(avail_profiles)))
237     _set_active_name(name)