Browse Source

feat(api): policy enforcement and api standard

- enhanced logging
- created base structure
- updated docs
- PasteDeploy auth
- Oslo Policy

Closes #107

Change-Id: I805863c57f17fcfb26dac5d03efb165e4be49a4e
changes/20/570020/1
gardlt 1 year ago
parent
commit
bb26131ce2

+ 1
- 2
Dockerfile View File

@@ -28,8 +28,7 @@ RUN apt-get update && \
28 28
     \
29 29
     apt-get purge --auto-remove -y \
30 30
       build-essential \
31
-      curl \
32
-      python-all-dev && \
31
+      curl && \
33 32
     apt-get clean -y && \
34 33
     rm -rf \
35 34
       /root/.cache \

+ 137
- 1
armada/api/__init__.py View File

@@ -1,4 +1,4 @@
1
-# Copyright 2017 The Armada Authors.
1
+# Copyright 2017 The Armada Authors.  All other rights reserved.
2 2
 #
3 3
 # Licensed under the Apache License, Version 2.0 (the "License");
4 4
 # you may not use this file except in compliance with the License.
@@ -11,3 +11,139 @@
11 11
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 12
 # See the License for the specific language governing permissions and
13 13
 # limitations under the License.
14
+
15
+import json
16
+import uuid
17
+import logging as log
18
+
19
+import falcon
20
+from oslo_log import log as logging
21
+
22
+LOG = logging.getLogger(__name__)
23
+
24
+
25
+class BaseResource(object):
26
+
27
+    def __init__(self):
28
+        self.logger = LOG
29
+
30
+    def on_options(self, req, resp):
31
+        self_attrs = dir(self)
32
+        methods = ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'PATCH']
33
+        allowed_methods = []
34
+
35
+        for m in methods:
36
+            if 'on_' + m.lower() in self_attrs:
37
+                allowed_methods.append(m)
38
+
39
+        resp.headers['Allow'] = ','.join(allowed_methods)
40
+        resp.status = falcon.HTTP_200
41
+
42
+    def req_json(self, req):
43
+        if req.content_length is None or req.content_length == 0:
44
+            return None
45
+
46
+        if req.content_type is not None and req.content_type.lower(
47
+        ) == 'application/json':
48
+            raw_body = req.stream.read(req.content_length or 0)
49
+
50
+            if raw_body is None:
51
+                return None
52
+
53
+            try:
54
+                # json_body = json.loads(raw_body.decode('utf-8'))
55
+                # return json_body
56
+                return raw_body
57
+            except json.JSONDecodeError as jex:
58
+                self.error(
59
+                    req.context,
60
+                    "Invalid JSON in request: \n%s" % raw_body.decode('utf-8'))
61
+                raise json.JSONDecodeError("%s: Invalid JSON in body: %s" %
62
+                                           (req.path, jex))
63
+        else:
64
+            raise json.JSONDecodeError("Requires application/json payload")
65
+
66
+    def return_error(self, resp, status_code, message="", retry=False):
67
+        resp.body = json.dumps({
68
+            'type': 'error',
69
+            'message': message,
70
+            'retry': retry
71
+        })
72
+        resp.status = status_code
73
+
74
+    def log_error(self, ctx, level, msg):
75
+        extra = {'user': 'N/A', 'req_id': 'N/A', 'external_ctx': 'N/A'}
76
+
77
+        if ctx is not None:
78
+            extra = {
79
+                'user': ctx.user,
80
+                'req_id': ctx.request_id,
81
+                'external_ctx': ctx.external_marker,
82
+            }
83
+
84
+        self.logger.log(level, msg, extra=extra)
85
+
86
+    def debug(self, ctx, msg):
87
+        self.log_error(ctx, log.DEBUG, msg)
88
+
89
+    def info(self, ctx, msg):
90
+        self.log_error(ctx, log.INFO, msg)
91
+
92
+    def warn(self, ctx, msg):
93
+        self.log_error(ctx, log.WARN, msg)
94
+
95
+    def error(self, ctx, msg):
96
+        self.log_error(ctx, log.ERROR, msg)
97
+
98
+
99
+class ArmadaRequestContext(object):
100
+    def __init__(self):
101
+        self.log_level = 'ERROR'
102
+        self.user = None  # Username
103
+        self.user_id = None  # User ID (UUID)
104
+        self.user_domain_id = None  # Domain owning user
105
+        self.roles = ['anyone']
106
+        self.project_id = None
107
+        self.project_domain_id = None  # Domain owning project
108
+        self.is_admin_project = False
109
+        self.authenticated = False
110
+        self.request_id = str(uuid.uuid4())
111
+        self.external_marker = ''
112
+
113
+    def set_log_level(self, level):
114
+        if level in ['error', 'info', 'debug']:
115
+            self.log_level = level
116
+
117
+    def set_user(self, user):
118
+        self.user = user
119
+
120
+    def set_project(self, project):
121
+        self.project = project
122
+
123
+    def add_role(self, role):
124
+        self.roles.append(role)
125
+
126
+    def add_roles(self, roles):
127
+        self.roles.extend(roles)
128
+
129
+    def remove_role(self, role):
130
+        self.roles = [x for x in self.roles if x != role]
131
+
132
+    def set_external_marker(self, marker):
133
+        self.external_marker = marker
134
+
135
+    def to_policy_view(self):
136
+        policy_dict = {}
137
+
138
+        policy_dict['user_id'] = self.user_id
139
+        policy_dict['user_domain_id'] = self.user_domain_id
140
+        policy_dict['project_id'] = self.project_id
141
+        policy_dict['project_domain_id'] = self.project_domain_id
142
+        policy_dict['roles'] = self.roles
143
+        policy_dict['is_admin_project'] = self.is_admin_project
144
+
145
+        return policy_dict
146
+
147
+
148
+class ArmadaRequest(falcon.request.Request):
149
+    context_type = ArmadaRequestContext

+ 33
- 27
armada/api/armada_controller.py View File

@@ -13,41 +13,47 @@
13 13
 # limitations under the License.
14 14
 
15 15
 import json
16
-from falcon import HTTP_200
17 16
 
18
-from oslo_config import cfg
17
+import falcon
19 18
 from oslo_log import log as logging
20 19
 
21
-from armada.handlers.armada import Armada as Handler
20
+from armada import api
21
+from armada.handlers.armada import Armada
22 22
 
23 23
 LOG = logging.getLogger(__name__)
24
-CONF = cfg.CONF
25 24
 
26
-
27
-class Apply(object):
25
+class Apply(api.BaseResource):
28 26
     '''
29 27
     apply armada endpoint service
30 28
     '''
31 29
 
32 30
     def on_post(self, req, resp):
33
-
34
-        # Load data from request and get options
35
-        data = json.load(req.stream)
36
-        opts = data['options']
37
-
38
-        # Encode filename
39
-        data['file'] = data['file'].encode('utf-8')
40
-
41
-        armada = Handler(open('../../' + data['file']),
42
-                         disable_update_pre=opts['disable_update_pre'],
43
-                         disable_update_post=opts['disable_update_post'],
44
-                         enable_chart_cleanup=opts['enable_chart_cleanup'],
45
-                         dry_run=opts['dry_run'],
46
-                         wait=opts['wait'],
47
-                         timeout=opts['timeout'])
48
-
49
-        armada.sync()
50
-
51
-        resp.data = json.dumps({'message': 'Success'})
52
-        resp.content_type = 'application/json'
53
-        resp.status = HTTP_200
31
+        try:
32
+
33
+            # Load data from request and get options
34
+            data = self.req_json(req)
35
+            opts = {}
36
+            # opts = data['options']
37
+
38
+            # Encode filename
39
+            # data['file'] = data['file'].encode('utf-8')
40
+            armada = Armada(
41
+                data,
42
+                disable_update_pre=opts.get('disable_update_pre', False),
43
+                disable_update_post=opts.get('disable_update_post', False),
44
+                enable_chart_cleanup=opts.get('enable_chart_cleanup', False),
45
+                dry_run=opts.get('dry_run', False),
46
+                wait=opts.get('wait', False),
47
+                timeout=opts.get('timeout', False))
48
+
49
+            msg = armada.sync()
50
+
51
+            resp.data = json.dumps({'message': msg})
52
+
53
+            resp.content_type = 'application/json'
54
+            resp.status = falcon.HTTP_200
55
+        except Exception as e:
56
+            self.error(req.context, "Failed to apply manifest")
57
+            self.return_error(
58
+                resp, falcon.HTTP_500,
59
+                message="Failed to install manifest: {} {}".format(e, data))

+ 68
- 65
armada/api/middleware.py View File

@@ -12,10 +12,8 @@
12 12
 # See the License for the specific language governing permissions and
13 13
 # limitations under the License.
14 14
 
15
-import falcon
15
+from uuid import UUID
16 16
 
17
-from keystoneauth1 import session
18
-from keystoneauth1.identity import v3
19 17
 from oslo_config import cfg
20 18
 from oslo_log import log as logging
21 19
 
@@ -25,75 +23,80 @@ CONF = cfg.CONF
25 23
 
26 24
 class AuthMiddleware(object):
27 25
 
26
+    # Authentication
28 27
     def process_request(self, req, resp):
28
+        ctx = req.context
29
+
30
+        for k, v in req.headers.items():
31
+            LOG.debug("Request with header %s: %s" % (k, v))
32
+
33
+        auth_status = req.get_header('X-SERVICE-IDENTITY-STATUS')
34
+        service = True
35
+
36
+        if auth_status is None:
37
+            auth_status = req.get_header('X-IDENTITY-STATUS')
38
+            service = False
39
+
40
+        if auth_status == 'Confirmed':
41
+            # Process account and roles
42
+            ctx.authenticated = True
43
+            ctx.user = req.get_header(
44
+                'X-SERVICE-USER-NAME') if service else req.get_header(
45
+                    'X-USER-NAME')
46
+            ctx.user_id = req.get_header(
47
+                'X-SERVICE-USER-ID') if service else req.get_header(
48
+                    'X-USER-ID')
49
+            ctx.user_domain_id = req.get_header(
50
+                'X-SERVICE-USER-DOMAIN-ID') if service else req.get_header(
51
+                    'X-USER-DOMAIN-ID')
52
+            ctx.project_id = req.get_header(
53
+                'X-SERVICE-PROJECT-ID') if service else req.get_header(
54
+                    'X-PROJECT-ID')
55
+            ctx.project_domain_id = req.get_header(
56
+                'X-SERVICE-PROJECT-DOMAIN-ID') if service else req.get_header(
57
+                    'X-PROJECT-DOMAIN-NAME')
58
+            if service:
59
+                ctx.add_roles(req.get_header('X-SERVICE-ROLES').split(','))
60
+            else:
61
+                ctx.add_roles(req.get_header('X-ROLES').split(','))
62
+
63
+            if req.get_header('X-IS-ADMIN-PROJECT') == 'True':
64
+                ctx.is_admin_project = True
65
+            else:
66
+                ctx.is_admin_project = False
67
+
68
+            LOG.debug('Request from authenticated user %s with roles %s' %
69
+                      (ctx.user, ','.join(ctx.roles)))
70
+        else:
71
+            ctx.authenticated = False
72
+
73
+
74
+class ContextMiddleware(object):
29 75
 
30
-        # Validate token and get user session
31
-        token = req.get_header('X-Auth-Token')
32
-        req.context['session'] = self._get_user_session(token)
33
-
34
-        # Add token roles to request context
35
-        req.context['roles'] = self._get_roles(req.context['session'])
36
-
37
-    def _get_roles(self, session):
38
-
39
-        # Get roles IDs associated with user
40
-        request_url = CONF.auth_url + '/role_assignments'
41
-        resp = self._session_request(session=session, request_url=request_url)
42
-
43
-        json_resp = resp.json()['role_assignments']
44
-        role_ids = [r['role']['id'].encode('utf-8') for r in json_resp]
45
-
46
-        # Get role names associated with role IDs
47
-        roles = []
48
-        for role_id in role_ids:
49
-            request_url = CONF.auth_url + '/roles/' + role_id
50
-            resp = self._session_request(session=session,
51
-                                         request_url=request_url)
52
-
53
-            role = resp.json()['role']['name'].encode('utf-8')
54
-            roles.append(role)
55
-
56
-        return roles
57
-
58
-    def _get_user_session(self, token):
76
+    def process_request(self, req, resp):
77
+        ctx = req.context
59 78
 
60
-        # Get user session from token
61
-        auth = v3.Token(auth_url=CONF.auth_url,
62
-                        project_name=CONF.project_name,
63
-                        project_domain_name=CONF.project_domain_name,
64
-                        token=token)
79
+        ext_marker = req.get_header('X-Context-Marker')
65 80
 
66
-        return session.Session(auth=auth)
81
+        if ext_marker is not None and self.is_valid_uuid(ext_marker):
82
+            ctx.set_external_marker(ext_marker)
67 83
 
68
-    def _session_request(self, session, request_url):
84
+    def is_valid_uuid(self, id, version=4):
69 85
         try:
70
-            return session.get(request_url)
86
+            uuid_obj = UUID(id, version=version)
71 87
         except:
72
-            raise falcon.HTTPUnauthorized('Authentication required',
73
-                                          ('Authentication token is invalid.'))
74
-
75
-class RoleMiddleware(object):
76
-
77
-    def process_request(self, req, resp):
78
-        endpoint = req.path
79
-        roles = req.context['roles']
80
-
81
-        # Verify roles have sufficient permissions for request endpoint
82
-        if not (self._verify_roles(endpoint, roles)):
83
-            raise falcon.HTTPUnauthorized('Insufficient permissions',
84
-                                          ('Token role insufficient.'))
85
-
86
-    def _verify_roles(self, endpoint, roles):
88
+            return False
87 89
 
88
-        # Compare the verified roles listed in the config with the user's
89
-        # associated roles
90
-        if endpoint == '/armada/apply':
91
-            approved_roles = CONF.armada_apply_roles
92
-        elif endpoint == '/tiller/releases':
93
-            approved_roles = CONF.tiller_release_roles
94
-        elif endpoint == '/tiller/status':
95
-            approved_roles = CONF.tiller_status_roles
90
+        return str(uuid_obj) == id
96 91
 
97
-        verified_roles = set(roles).intersection(approved_roles)
98 92
 
99
-        return bool(verified_roles)
93
+class LoggingMiddleware(object):
94
+    def process_response(self, req, resp, resource, req_succeeded):
95
+        ctx = req.context
96
+        extra = {
97
+            'user': ctx.user,
98
+            'req_id': ctx.request_id,
99
+            'external_ctx': ctx.external_marker,
100
+        }
101
+        resp.append_header('X-Armada-Req', ctx.request_id)
102
+        LOG.info("%s - %s" % (req.uri, resp.status), extra=extra)

+ 37
- 16
armada/api/server.py View File

@@ -13,42 +13,63 @@
13 13
 # limitations under the License.
14 14
 
15 15
 import falcon
16
+import os
16 17
 
17 18
 from oslo_config import cfg
18 19
 from oslo_log import log as logging
19 20
 
20
-import armada.conf as configs
21
+from armada.common import policy
22
+from armada import conf
21 23
 
24
+from armada.api import ArmadaRequest
22 25
 from armada_controller import Apply
23 26
 from middleware import AuthMiddleware
24
-from middleware import RoleMiddleware
27
+from middleware import ContextMiddleware
28
+from middleware import LoggingMiddleware
25 29
 from tiller_controller import Release
26 30
 from tiller_controller import Status
31
+from validation_controller import Validate
27 32
 
28 33
 LOG = logging.getLogger(__name__)
29
-configs.set_app_default_configs()
34
+conf.set_app_default_configs()
30 35
 CONF = cfg.CONF
31 36
 
37
+
32 38
 # Build API
33 39
 def create(middleware=CONF.middleware):
34
-    logging.register_options(CONF)
35
-    logging.set_defaults(default_log_levels=CONF.default_log_levels)
36
-    logging.setup(CONF, 'armada')
40
+    if not (os.path.exists('etc/armada/armada.conf')):
41
+        logging.register_options(CONF)
42
+        logging.set_defaults(default_log_levels=CONF.default_log_levels)
43
+        logging.setup(CONF, 'armada')
44
+
45
+    policy.setup_policy()
37 46
 
38 47
     if middleware:
39
-        api = falcon.API(middleware=[AuthMiddleware(), RoleMiddleware()])
48
+        api = falcon.API(
49
+            request_type=ArmadaRequest,
50
+            middleware=[
51
+                AuthMiddleware(),
52
+                LoggingMiddleware(),
53
+                ContextMiddleware()
54
+            ])
40 55
     else:
41
-        api = falcon.API()
56
+        api = falcon.API(request_type=ArmadaRequest)
42 57
 
43 58
     # Configure API routing
44
-    url_routes = (
45
-        ('/tiller/status', Status()),
46
-        ('/tiller/releases', Release()),
47
-        ('/armada/apply/', Apply())
48
-    )
49
-
50
-    for route, service in url_routes:
51
-        api.add_route(route, service)
59
+    url_routes_v1 = (('apply', Apply()),
60
+                     ('releases', Release()),
61
+                     ('status', Status()),
62
+                     ('validate', Validate()))
63
+
64
+    for route, service in url_routes_v1:
65
+        api.add_route("/v1.0/{}".format(route), service)
66
+
67
+    return api
68
+
69
+
70
+def paste_start_armada(global_conf, **kwargs):
71
+    # At this time just ignore everything in  the paste configuration
72
+    # and rely on olso_config
52 73
 
53 74
     return api
54 75
 

+ 44
- 23
armada/api/tiller_controller.py View File

@@ -13,45 +13,66 @@
13 13
 # limitations under the License.
14 14
 
15 15
 import json
16
-from falcon import HTTP_200
17 16
 
17
+import falcon
18 18
 from oslo_config import cfg
19 19
 from oslo_log import log as logging
20 20
 
21
-from armada.handlers.tiller import Tiller as tillerHandler
21
+from armada import api
22
+from armada.common import policy
23
+from armada.handlers.tiller import Tiller
22 24
 
23 25
 LOG = logging.getLogger(__name__)
24 26
 CONF = cfg.CONF
25 27
 
26 28
 
27
-class Status(object):
29
+class Status(api.BaseResource):
30
+    @policy.enforce('tiller:get_status')
28 31
     def on_get(self, req, resp):
29 32
         '''
30 33
         get tiller status
31 34
         '''
32
-        message = "Tiller Server is {}"
33
-        if tillerHandler().tiller_status():
34
-            resp.data = json.dumps({'message': message.format('Active')})
35
-            LOG.info('Tiller Server is Active.')
36
-        else:
37
-            resp.data = json.dumps({'message': message.format('Not Present')})
38
-            LOG.info('Tiller Server is Not Present.')
39
-
40
-        resp.content_type = 'application/json'
41
-        resp.status = HTTP_200
42
-
43
-class Release(object):
35
+        try:
36
+            message = {'tiller': Tiller().tiller_status()}
37
+
38
+            if message.get('tiller', False):
39
+                resp.status = falcon.HTTP_200
40
+            else:
41
+                resp.status = falcon.HTTP_503
42
+
43
+            resp.data = json.dumps(message)
44
+            resp.content_type = 'application/json'
45
+
46
+        except Exception as e:
47
+            self.error(req.context, "Unable to find resources")
48
+            self.return_error(
49
+                resp, falcon.HTTP_500,
50
+                message="Unable to get status: {}".format(e))
51
+
52
+
53
+class Release(api.BaseResource):
54
+    @policy.enforce('tiller:get_release')
44 55
     def on_get(self, req, resp):
45 56
         '''
46 57
         get tiller releases
47 58
         '''
48
-        # Get tiller releases
49
-        handler = tillerHandler()
59
+        try:
60
+            # Get tiller releases
61
+            handler = Tiller()
62
+
63
+            releases = {}
64
+            for release in handler.list_releases():
65
+                if not releases.get(release.namespace, None):
66
+                    releases[release.namespace] = []
67
+
68
+                releases[release.namespace].append(release.name)
50 69
 
51
-        releases = {}
52
-        for release in handler.list_releases():
53
-            releases[release.name] = release.namespace
70
+            resp.data = json.dumps({'releases': releases})
71
+            resp.content_type = 'application/json'
72
+            resp.status = falcon.HTTP_200
54 73
 
55
-        resp.data = json.dumps({'releases': releases})
56
-        resp.content_type = 'application/json'
57
-        resp.status = HTTP_200
74
+        except Exception as e:
75
+            self.error(req.context, "Unable to find resources")
76
+            self.return_error(
77
+                resp, falcon.HTTP_500,
78
+                message="Unable to find Releases: {}".format(e))

+ 56
- 0
armada/api/validation_controller.py View File

@@ -0,0 +1,56 @@
1
+# Copyright 2017 The Armada Authors.
2
+#
3
+# Licensed under the Apache License, Version 2.0 (the "License");
4
+# you may not use this file except in compliance with the License.
5
+# You may obtain a copy of the License at
6
+#
7
+#     http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+# Unless required by applicable law or agreed to in writing, software
10
+# distributed under the License is distributed on an "AS IS" BASIS,
11
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+# See the License for the specific language governing permissions and
13
+# limitations under the License.
14
+
15
+import json
16
+import yaml
17
+
18
+import falcon
19
+from oslo_log import log as logging
20
+
21
+from armada import api
22
+from armada.common import policy
23
+from armada.utils.lint import validate_armada_documents
24
+
25
+LOG = logging.getLogger(__name__)
26
+
27
+
28
+class Validate(api.BaseResource):
29
+    '''
30
+    apply armada endpoint service
31
+    '''
32
+
33
+    @policy.enforce('armada:validate_manifest')
34
+    def on_post(self, req, resp):
35
+        try:
36
+
37
+            message = {
38
+                'valid':
39
+                validate_armada_documents(
40
+                    list(yaml.safe_load_all(self.req_json(req))))
41
+            }
42
+
43
+            if message.get('valid', False):
44
+                resp.status = falcon.HTTP_200
45
+            else:
46
+                resp.status = falcon.HTTP_400
47
+
48
+            resp.data = json.dumps(message)
49
+            resp.content_type = 'application/json'
50
+
51
+        except Exception:
52
+            self.error(req.context, "Failed: Invalid Armada Manifest")
53
+            self.return_error(
54
+                resp,
55
+                falcon.HTTP_400,
56
+                message="Failed: Invalid Armada Manifest")

+ 0
- 0
armada/common/__init__.py View File


+ 19
- 0
armada/common/i18n.py View File

@@ -0,0 +1,19 @@
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.
12
+
13
+import oslo_i18n
14
+
15
+
16
+_translators = oslo_i18n.TranslatorFactory(domain='armada')
17
+
18
+# The primary translation function using the well-known name "_"
19
+_ = _translators.primary

+ 25
- 0
armada/common/policies/__init__.py View File

@@ -0,0 +1,25 @@
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.
12
+
13
+import itertools
14
+
15
+from armada.common.policies import base
16
+from armada.common.policies import service
17
+from armada.common.policies import tiller
18
+
19
+
20
+def list_rules():
21
+    return itertools.chain(
22
+        base.list_rules(),
23
+        service.list_rules(),
24
+        tiller.list_rules()
25
+    )

+ 34
- 0
armada/common/policies/base.py View File

@@ -0,0 +1,34 @@
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.
12
+
13
+from oslo_policy import policy
14
+
15
+ARMADA = 'armada:%s'
16
+TILLER = 'tiller:%s'
17
+RULE_ADMIN_REQUIRED = 'rule:admin_required'
18
+RULE_ADMIN_OR_TARGET_PROJECT = (
19
+    'rule:admin_required or project_id:%(target.project.id)s')
20
+RULE_SERVICE_OR_ADMIN = 'rule:service_or_admin'
21
+
22
+
23
+rules = [
24
+    policy.RuleDefault(name='admin_required',
25
+                       check_str='role:admin'),
26
+    policy.RuleDefault(name='service_or_admin',
27
+                       check_str='rule:admin_required or rule:service_role'),
28
+    policy.RuleDefault(name='service_role',
29
+                       check_str='role:service'),
30
+]
31
+
32
+
33
+def list_rules():
34
+    return rules

+ 33
- 0
armada/common/policies/service.py View File

@@ -0,0 +1,33 @@
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.
12
+
13
+from oslo_policy import policy
14
+
15
+from armada.common.policies import base
16
+
17
+
18
+armada_policies = [
19
+    policy.DocumentedRuleDefault(
20
+        name=base.ARMADA % 'create_endpoints',
21
+        check_str=base.RULE_ADMIN_REQUIRED,
22
+        description='install manifest charts',
23
+        operations=[{'path': '/v1.0/apply/', 'method': 'POST'}]),
24
+    policy.DocumentedRuleDefault(
25
+        name=base.ARMADA % 'validate_manifest',
26
+        check_str=base.RULE_ADMIN_REQUIRED,
27
+        description='validate install manifest',
28
+        operations=[{'path': '/v1.0/validate/', 'method': 'POST'}]),
29
+]
30
+
31
+
32
+def list_rules():
33
+    return armada_policies

+ 36
- 0
armada/common/policies/tiller.py View File

@@ -0,0 +1,36 @@
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.
12
+
13
+from oslo_policy import policy
14
+
15
+from armada.common.policies import base
16
+
17
+
18
+tiller_policies = [
19
+    policy.DocumentedRuleDefault(
20
+        name=base.TILLER % 'get_status',
21
+        check_str=base.RULE_ADMIN_REQUIRED,
22
+        description='Get tiller status',
23
+        operations=[{'path': '/v1.0/status/',
24
+                     'method': 'GET'}]),
25
+
26
+    policy.DocumentedRuleDefault(
27
+        name=base.TILLER % 'get_release',
28
+        check_str=base.RULE_ADMIN_REQUIRED,
29
+        description='Get tiller release',
30
+        operations=[{'path': '/v1.0/releases/',
31
+                     'method': 'GET'}]),
32
+]
33
+
34
+
35
+def list_rules():
36
+    return tiller_policies

+ 57
- 0
armada/common/policy.py View File

@@ -0,0 +1,57 @@
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.
12
+
13
+import functools
14
+
15
+from oslo_config import cfg
16
+from oslo_policy import policy
17
+
18
+from armada.common import policies
19
+from armada.exceptions import base_exception as exc
20
+
21
+
22
+CONF = cfg.CONF
23
+_ENFORCER = None
24
+
25
+
26
+def setup_policy():
27
+    global _ENFORCER
28
+    if not _ENFORCER:
29
+        _ENFORCER = policy.Enforcer(CONF)
30
+        register_rules(_ENFORCER)
31
+
32
+
33
+def enforce_policy(action, target, credentials, do_raise=True):
34
+    extras = {}
35
+    if do_raise:
36
+        extras.update(exc=exc.ActionForbidden, do_raise=do_raise)
37
+
38
+    _ENFORCER.enforce(action, target, credentials.to_policy_view(), **extras)
39
+
40
+
41
+def enforce(rule):
42
+
43
+    setup_policy()
44
+
45
+    def decorator(func):
46
+        @functools.wraps(func)
47
+        def handler(*args, **kwargs):
48
+            context = args[1].context
49
+            enforce_policy(rule, {}, context, do_raise=True)
50
+            return func(*args, **kwargs)
51
+        return handler
52
+
53
+    return decorator
54
+
55
+
56
+def register_rules(enforcer):
57
+    enforcer.register_defaults(policies.list_rules())

+ 32
- 0
armada/exceptions/api_exceptions.py View File

@@ -0,0 +1,32 @@
1
+# Copyright 2017 The Armada Authors.
2
+#
3
+# Licensed under the Apache License, Version 2.0 (the "License");
4
+# you may not use this file except in compliance with the License.
5
+# You may obtain a copy of the License at
6
+#
7
+#     http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+# Unless required by applicable law or agreed to in writing, software
10
+# distributed under the License is distributed on an "AS IS" BASIS,
11
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+# See the License for the specific language governing permissions and
13
+# limitations under the License.
14
+
15
+import base_exception as base
16
+
17
+class ApiException(base.ArmadaBaseException):
18
+    '''Base class for API exceptions and error handling.'''
19
+
20
+    message = 'An unknown API error occur.'
21
+
22
+
23
+class ApiBaseException(ApiException):
24
+    '''Exception that occurs during chart cleanup.'''
25
+
26
+    message = 'There was an error listing the helm chart releases.'
27
+
28
+
29
+class ApiJsonException(ApiException):
30
+    '''Exception that occurs during chart cleanup.'''
31
+
32
+    message = 'There was an error listing the helm chart releases.'

+ 21
- 0
armada/exceptions/base_exception.py View File

@@ -12,9 +12,12 @@
12 12
 # See the License for the specific language governing permissions and
13 13
 # limitations under the License.
14 14
 
15
+import falcon
15 16
 from oslo_config import cfg
16 17
 from oslo_log import log as logging
17 18
 
19
+from armada.common.i18n import _
20
+
18 21
 LOG = logging.getLogger(__name__)
19 22
 
20 23
 DEFAULT_TIMEOUT = 3600
@@ -27,3 +30,21 @@ class ArmadaBaseException(Exception):
27 30
     def __init__(self, message=None):
28 31
         self.message = message or self.message
29 32
         super(ArmadaBaseException, self).__init__(self.message)
33
+
34
+
35
+class ArmadaAPIException(falcon.HTTPError):
36
+    status = falcon.HTTP_500
37
+    message = "unknown error"
38
+    title = "Internal Server Error"
39
+
40
+    def __init__(self, message=None, **kwargs):
41
+        self.message = message or self.message
42
+        super(ArmadaAPIException, self).__init__(
43
+            self.status, self.title, self.message, **kwargs
44
+        )
45
+
46
+
47
+class ActionForbidden(ArmadaAPIException):
48
+    status = falcon.HTTP_403
49
+    message = _("Insufficient privilege to perform action.")
50
+    title = _("Action Forbidden")

+ 0
- 13
armada/handlers/__init__.py View File

@@ -1,13 +0,0 @@
1
-# Copyright 2017 The Armada Authors.
2
-#
3
-# Licensed under the Apache License, Version 2.0 (the "License");
4
-# you may not use this file except in compliance with the License.
5
-# You may obtain a copy of the License at
6
-#
7
-#     http://www.apache.org/licenses/LICENSE-2.0
8
-#
9
-# Unless required by applicable law or agreed to in writing, software
10
-# distributed under the License is distributed on an "AS IS" BASIS,
11
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
-# See the License for the specific language governing permissions and
13
-# limitations under the License.

+ 32
- 9
armada/handlers/armada.py View File

@@ -15,7 +15,6 @@
15 15
 import difflib
16 16
 import yaml
17 17
 
18
-from oslo_config import cfg
19 18
 from oslo_log import log as logging
20 19
 from supermutes.dot import dotify
21 20
 
@@ -37,7 +36,6 @@ from ..const import KEYWORD_ARMADA, KEYWORD_GROUPS, KEYWORD_CHARTS,\
37 36
 LOG = logging.getLogger(__name__)
38 37
 
39 38
 DEFAULT_TIMEOUT = 3600
40
-CONF = cfg.CONF
41 39
 
42 40
 
43 41
 class Armada(object):
@@ -183,6 +181,12 @@ class Armada(object):
183 181
         Syncronize Helm with the Armada Config(s)
184 182
         '''
185 183
 
184
+        msg = {
185
+            'installed': [],
186
+            'upgraded': [],
187
+            'diff': []
188
+        }
189
+
186 190
         # TODO: (gardlt) we need to break up this func into
187 191
         # a more cleaner format
188 192
         LOG.info("Performing Pre-Flight Operations")
@@ -268,9 +272,9 @@ class Armada(object):
268 272
                     # TODO(alanmeadows) account for .files differences
269 273
                     # once we support those
270 274
 
271
-                    upgrade_diff = self.show_diff(chart, apply_chart,
272
-                                                  apply_values,
273
-                                                  chartbuilder.dump(), values)
275
+                    upgrade_diff = self.show_diff(
276
+                        chart, apply_chart, apply_values, chartbuilder.dump(),
277
+                        values, msg)
274 278
 
275 279
                     if not upgrade_diff:
276 280
                         LOG.info("There are no updates found in this chart")
@@ -290,6 +294,8 @@ class Armada(object):
290 294
                                                wait=chart_wait,
291 295
                                                timeout=chart_timeout)
292 296
 
297
+                    msg['upgraded'].append(prefix_chart)
298
+
293 299
                 # process install
294 300
                 else:
295 301
                     LOG.info("Installing release %s", chart.release)
@@ -301,6 +307,8 @@ class Armada(object):
301 307
                                                 wait=chart_wait,
302 308
                                                 timeout=chart_timeout)
303 309
 
310
+                    msg['installed'].append(prefix_chart)
311
+
304 312
                 LOG.debug("Cleaning up chart source in %s",
305 313
                           chartbuilder.source_directory)
306 314
 
@@ -322,6 +330,8 @@ class Armada(object):
322 330
             self.tiller.chart_cleanup(
323 331
                 prefix, self.config[KEYWORD_ARMADA][KEYWORD_GROUPS])
324 332
 
333
+        return msg
334
+
325 335
     def post_flight_ops(self):
326 336
         '''
327 337
         Operations to run after deployment process has terminated
@@ -333,7 +343,7 @@ class Armada(object):
333 343
                     source.source_cleanup(ch.get('chart').get('source_dir')[0])
334 344
 
335 345
     def show_diff(self, chart, installed_chart, installed_values, target_chart,
336
-                  target_values):
346
+                  target_values, msg):
337 347
         '''
338 348
         Produce a unified diff of the installed chart vs our intention
339 349
 
@@ -342,19 +352,32 @@ class Armada(object):
342 352
         '''
343 353
 
344 354
         chart_diff = list(
345
-            difflib.unified_diff(installed_chart.SerializeToString()
346
-                                 .split('\n'), target_chart.split('\n')))
355
+            difflib.unified_diff(
356
+                installed_chart.SerializeToString().split('\n'),
357
+                target_chart.split('\n')))
358
+
347 359
         if len(chart_diff) > 0:
348 360
             LOG.info("Chart Unified Diff (%s)", chart.release)
361
+            diff_msg = []
349 362
             for line in chart_diff:
363
+                diff_msg.append(line)
350 364
                 LOG.debug(line)
365
+
366
+            msg['diff'].append({'chart': diff_msg})
367
+
351 368
         values_diff = list(
352 369
             difflib.unified_diff(
353 370
                 installed_values.split('\n'),
354 371
                 yaml.safe_dump(target_values).split('\n')))
372
+
355 373
         if len(values_diff) > 0:
356 374
             LOG.info("Values Unified Diff (%s)", chart.release)
375
+            diff_msg = []
357 376
             for line in values_diff:
377
+                diff_msg.append(line)
358 378
                 LOG.debug(line)
379
+                msg['diff'].append({'values': diff_msg})
380
+
381
+        result = (len(chart_diff) > 0) or (len(values_diff) > 0)
359 382
 
360
-        return (len(chart_diff) > 0) or (len(values_diff) > 0)
383
+        return result

+ 28
- 9
armada/tests/unit/api/test_api.py View File

@@ -16,16 +16,21 @@ import json
16 16
 import mock
17 17
 import unittest
18 18
 
19
+import falcon
19 20
 from falcon import testing
20 21
 
22
+from armada import conf as cfg
21 23
 from armada.api import server
22 24
 
25
+CONF = cfg.CONF
26
+
27
+
23 28
 class APITestCase(testing.TestCase):
24 29
     def setUp(self):
25 30
         super(APITestCase, self).setUp()
26
-
27 31
         self.app = server.create(middleware=False)
28 32
 
33
+
29 34
 class TestAPI(APITestCase):
30 35
     @unittest.skip('this is incorrectly tested')
31 36
     @mock.patch('armada.api.armada_controller.Handler')
@@ -35,7 +40,7 @@ class TestAPI(APITestCase):
35 40
         '''
36 41
         mock_armada.sync.return_value = None
37 42
 
38
-        body = json.dumps({'file': '../examples/openstack-helm.yaml',
43
+        body = json.dumps({'file': '',
39 44
                            'options': {'debug': 'true',
40 45
                                        'disable_update_pre': 'false',
41 46
                                        'disable_update_post': 'false',
@@ -50,10 +55,10 @@ class TestAPI(APITestCase):
50 55
         result = self.simulate_post(path='/armada/apply', body=body)
51 56
         self.assertEqual(result.json, doc)
52 57
 
53
-    @mock.patch('armada.api.tiller_controller.tillerHandler')
58
+    @mock.patch('armada.api.tiller_controller.Tiller')
54 59
     def test_tiller_status(self, mock_tiller):
55 60
         '''
56
-        Test /tiller/status endpoint
61
+        Test /status endpoint
57 62
         '''
58 63
 
59 64
         # Mock tiller status value
@@ -61,10 +66,17 @@ class TestAPI(APITestCase):
61 66
 
62 67
         doc = {u'message': u'Tiller Server is Active'}
63 68
 
64
-        result = self.simulate_get('/tiller/status')
65
-        self.assertEqual(result.json, doc)
69
+        result = self.simulate_get('/v1.0/status')
66 70
 
67
-    @mock.patch('armada.api.tiller_controller.tillerHandler')
71
+        # TODO(lamt) This should be HTTP_401 if no auth is happening, but auth
72
+        # is not implemented currently, so it falls back to a policy check
73
+        # failure, thus a 403.  Change this once it is completed
74
+        self.assertEqual(falcon.HTTP_403, result.status)
75
+
76
+        # FIXME(lamt) Need authentication - mock, fixture
77
+        # self.assertEqual(result.json, doc)
78
+
79
+    @mock.patch('armada.api.tiller_controller.Tiller')
68 80
     def test_tiller_releases(self, mock_tiller):
69 81
         '''
70 82
         Test /tiller/releases endpoint
@@ -75,5 +87,12 @@ class TestAPI(APITestCase):
75 87
 
76 88
         doc = {u'releases': {}}
77 89
 
78
-        result = self.simulate_get('/tiller/releases')
79
-        self.assertEqual(result.json, doc)
90
+        result = self.simulate_get('/v1.0/releases')
91
+
92
+        # TODO(lamt) This should be HTTP_401 if no auth is happening, but auth
93
+        # is not implemented currently, so it falls back to a policy check
94
+        # failure, thus a 403.  Change this once it is completed
95
+        self.assertEqual(falcon.HTTP_403, result.status)
96
+
97
+        # FIXME(lamt) Need authentication - mock, fixture
98
+        # self.assertEqual(result.json, doc)

+ 65
- 0
armada/tests/unit/test_policy.py View File

@@ -0,0 +1,65 @@
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.
12
+
13
+import testtools
14
+
15
+from oslo_policy import policy as common_policy
16
+
17
+from armada.common import policy
18
+from armada import conf as cfg
19
+from armada.exceptions import base_exception as exc
20
+import mock
21
+
22
+
23
+CONF = cfg.CONF
24
+
25
+
26
+class PolicyTestCase(testtools.TestCase):
27
+
28
+    def setUp(self):
29
+        super(PolicyTestCase, self).setUp()
30
+        self.rules = {
31
+            "true": [],
32
+            "example:allowed": [],
33
+            "example:disallowed": [["false:false"]]
34
+        }
35
+        self._set_rules()
36
+        self.credentials = {}
37
+        self.target = {}
38
+
39
+    def _set_rules(self):
40
+        curr_rules = common_policy.Rules.from_dict(self.rules)
41
+        policy._ENFORCER.set_rules(curr_rules)
42
+
43
+    @mock.patch('armada.api.ArmadaRequestContext')
44
+    def test_enforce_nonexistent_action(self, mock_ctx):
45
+        action = "example:nope"
46
+        mock_ctx.to_policy_view.return_value = self.credentials
47
+
48
+        self.assertRaises(
49
+            exc.ActionForbidden, policy.enforce_policy, action,
50
+            self.target, mock_ctx)
51
+
52
+    @mock.patch('armada.api.ArmadaRequestContext')
53
+    def test_enforce_good_action(self, mock_ctx):
54
+        action = "example:allowed"
55
+        mock_ctx.to_policy_view.return_value = self.credentials
56
+
57
+        policy.enforce_policy(action, self.target, mock_ctx)
58
+
59
+    @mock.patch('armada.api.ArmadaRequestContext')
60
+    def test_enforce_bad_action(self, mock_ctx):
61
+        action = "example:disallowed"
62
+        mock_ctx.to_policy_view.return_value = self.credentials
63
+
64
+        self.assertRaises(exc.ActionForbidden, policy.enforce_policy,
65
+                          action, self.target, mock_ctx)

+ 24
- 6
docs/source/development/getting-started.rst View File

@@ -13,9 +13,17 @@ To use the docker containter to develop:
13 13
 
14 14
 .. code-block:: bash
15 15
 
16
+    git clone http://github.com/att-comdev/armada.git
17
+    cd armada
18
+
19
+    pip install tox
20
+
21
+    tox -e genconfig
22
+    tox -e genpolicy
23
+
16 24
     docker build . -t armada/latest
17 25
 
18
-    docker run -d --name armada -v ~/.kube/config:/armada/.kube/config -v $(pwd)/examples/:/examples armada/latest
26
+    docker run -d --name armada -v ~/.kube/config:/armada/.kube/config -v $(pwd)/etc:/armada/etc armada:local
19 27
 
20 28
 .. note::
21 29
 
@@ -25,17 +33,21 @@ To use the docker containter to develop:
25 33
 Virtualenv
26 34
 ##########
27 35
 
28
-To use VirtualEnv:
36
+How to set up armada in your local using virtualenv:
37
+
38
+.. note::
29 39
 
30
-1. virtualenv venv
31
-2. source ./venv/bin/activate
40
+    Suggest that you use a Ubuntu 16.04 VM
32 41
 
33 42
 From the directory of the forked repository:
34 43
 
35 44
 .. code-block:: bash
36 45
 
37
-    pip install -r requirements.txt
38
-    pip install -r test-requirements.txt
46
+
47
+    git clone http://github.com/att-comdev/armada.git && cd armada
48
+    virtualenv venv
49
+
50
+    pip install -r requirements.txt -r test-requirements.txt
39 51
 
40 52
     pip install .
41 53
 
@@ -48,6 +60,12 @@ From the directory of the forked repository:
48 60
     tox -e bandit
49 61
     tox -e cover
50 62
 
63
+
64
+    # policy and config are used in order to use and configure Armada API
65
+    tox -e genconfig
66
+    tox -e genpolicy
67
+
68
+
51 69
 .. note::
52 70
 
53 71
     If building from source, Armada requires that git be installed on

+ 52
- 0
docs/source/operations/guide-configure.rst View File

@@ -0,0 +1,52 @@
1
+==================
2
+Configuring Armada
3
+==================
4
+
5
+
6
+Armada uses an INI-like standard oslo_config file. A sample
7
+file can be generated via tox
8
+
9
+.. code-block:: bash
10
+
11
+    $ tox -e genconfig
12
+
13
+Customize your configuration based on the information below
14
+
15
+Keystone Integration
16
+====================
17
+
18
+Armada requires a service account to use for validating API
19
+tokens
20
+
21
+.. note::
22
+
23
+    If you do not have a keystone already deploy, then armada can deploy a keystone service.
24
+
25
+    armada apply keystone-manifest.yaml
26
+
27
+.. code-block:: bash
28
+
29
+    $ openstack domain create 'ucp'
30
+    $ openstack project create --domain 'ucp' 'service'
31
+    $ openstack user create --domain ucp --project service --project-domain 'ucp' --password armada armada
32
+    $ openstack role add --project-domain ucp --user-domain ucp --user armada --project service admin
33
+
34
+    # OR
35
+
36
+    $ ./tools/keystone-account.sh
37
+
38
+The service account must then be included in the armada.conf
39
+
40
+.. code-block:: ini
41
+
42
+    [keystone_authtoken]
43
+    auth_uri = https://<keystone-api>:5000/
44
+    auth_version = 3
45
+    delay_auth_decision = true
46
+    auth_type = password
47
+    auth_url = https://<keystone-api>:35357/
48
+    project_name = service
49
+    project_domain_name = ucp
50
+    user_name = armada
51
+    user_domain_name = ucp
52
+    password = armada

+ 3
- 2
docs/source/operations/index.rst View File

@@ -10,7 +10,8 @@ Operations Guide
10 10
    :maxdepth: 2
11 11
    :caption: Contents:
12 12
 
13
-   guide-troubleshooting.rst
13
+   guide-api.rst
14 14
    guide-build-armada-yaml.rst
15
+   guide-configure.rst
16
+   guide-troubleshooting.rst
15 17
    guide-use-armada.rst
16
-   guide-api.rst

+ 1
- 1
entrypoint.sh View File

@@ -6,7 +6,7 @@ PORT="8000"
6 6
 set -e
7 7
 
8 8
 if [ "$1" = 'server' ]; then
9
-    exec gunicorn server:api -b :$PORT --chdir armada/api
9
+    exec uwsgi --http 0.0.0.0:${PORT} --paste config:$(pwd)/etc/armada/api-paste.ini --enable-threads -L --pyargv " --config-file $(pwd)/etc/armada/armada.conf"
10 10
 fi
11 11
 
12 12
 if [ "$1" = 'tiller' ] || [ "$1" = 'apply' ]; then

+ 8
- 0
etc/armada/api-paste.ini View File

@@ -0,0 +1,8 @@
1
+[app:armada-api]
2
+paste.app_factory = armada.api.server:paste_start_armada
3
+
4
+[pipeline:main]
5
+pipeline = authtoken armada-api
6
+
7
+[filter:authtoken]
8
+paste.filter_factory = keystonemiddleware.auth_token:filter_factory

+ 0
- 140
etc/armada/armada.conf.sample View File

@@ -1,140 +0,0 @@
1
-[DEFAULT]
2
-
3
-#
4
-# From armada.conf
5
-#
6
-
7
-# IDs of approved API access roles. (list value)
8
-#armada_apply_roles = admin
9
-
10
-# The default Keystone authentication url. (string value)
11
-#auth_url = http://0.0.0.0/v3
12
-
13
-# Path to Kubernetes configurations. (string value)
14
-#kubernetes_config_path = /home/user/.kube/
15
-
16
-# Enables or disables Keystone authentication middleware. (boolean value)
17
-#middleware = true
18
-
19
-# The Keystone project domain name used for authentication. (string value)
20
-#project_domain_name = default
21
-
22
-# The Keystone project name used for authentication. (string value)
23
-#project_name = admin
24
-
25
-# Path to SSH private key. (string value)
26
-#ssh_key_path = /home/user/.ssh/
27
-
28
-# IDs of approved API access roles. (list value)
29
-#tiller_release_roles = admin
30
-
31
-# IDs of approved API access roles. (list value)
32
-#tiller_status_roles = admin
33
-
34
-#
35
-# From oslo.log
36
-#
37
-
38
-# If set to true, the logging level will be set to DEBUG instead of the default
39
-# INFO level. (boolean value)
40
-# Note: This option can be changed without restarting.
41
-#debug = false
42
-
43
-# The name of a logging configuration file. This file is appended to any
44
-# existing logging configuration files. For details about logging configuration
45
-# files, see the Python logging module documentation. Note that when logging
46
-# configuration files are used then all logging configuration is set in the
47
-# configuration file and other logging configuration options are ignored (for
48
-# example, logging_context_format_string). (string value)
49
-# Note: This option can be changed without restarting.
50
-# Deprecated group/name - [DEFAULT]/log_config
51
-#log_config_append = <None>
52
-
53
-# Defines the format string for %%(asctime)s in log records. Default:
54
-# %(default)s . This option is ignored if log_config_append is set. (string
55
-# value)
56
-#log_date_format = %Y-%m-%d %H:%M:%S
57
-
58
-# (Optional) Name of log file to send logging output to. If no default is set,
59
-# logging will go to stderr as defined by use_stderr. This option is ignored if
60
-# log_config_append is set. (string value)
61
-# Deprecated group/name - [DEFAULT]/logfile
62
-#log_file = <None>
63
-
64
-# (Optional) The base directory used for relative log_file  paths. This option
65
-# is ignored if log_config_append is set. (string value)
66
-# Deprecated group/name - [DEFAULT]/logdir
67
-#log_dir = <None>
68
-
69
-# Uses logging handler designed to watch file system. When log file is moved or
70
-# removed this handler will open a new log file with specified path
71
-# instantaneously. It makes sense only if log_file option is specified and Linux
72
-# platform is used. This option is ignored if log_config_append is set. (boolean
73
-# value)
74
-#watch_log_file = false
75
-
76
-# Use syslog for logging. Existing syslog format is DEPRECATED and will be
77
-# changed later to honor RFC5424. This option is ignored if log_config_append is
78
-# set. (boolean value)
79
-#use_syslog = false
80
-
81
-# Enable journald for logging. If running in a systemd environment you may wish
82
-# to enable journal support. Doing so will use the journal native protocol which
83
-# includes structured metadata in addition to log messages.This option is
84
-# ignored if log_config_append is set. (boolean value)
85
-#use_journal = false
86
-
87
-# Syslog facility to receive log lines. This option is ignored if
88
-# log_config_append is set. (string value)
89
-#syslog_log_facility = LOG_USER
90
-
91
-# Log output to standard error. This option is ignored if log_config_append is
92
-# set. (boolean value)
93
-#use_stderr = false
94
-
95
-# Format string to use for log messages with context. (string value)
96
-#logging_context_format_string = %(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s [%(request_id)s %(user_identity)s] %(instance)s%(message)s
97
-
98
-# Format string to use for log messages when context is undefined. (string
99
-# value)
100
-#logging_default_format_string = %(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s [-] %(instance)s%(message)s
101
-
102
-# Additional data to append to log message when logging level for the message is
103
-# DEBUG. (string value)
104
-#logging_debug_format_suffix = %(funcName)s %(pathname)s:%(lineno)d
105
-
106
-# Prefix each line of exception output with this format. (string value)
107
-#logging_exception_prefix = %(asctime)s.%(msecs)03d %(process)d ERROR %(name)s %(instance)s
108
-
109
-# Defines the format string for %(user_identity)s that is used in
110
-# logging_context_format_string. (string value)
111
-#logging_user_identity_format = %(user)s %(tenant)s %(domain)s %(user_domain)s %(project_domain)s
112
-
113
-# List of package logging levels in logger=LEVEL pairs. This option is ignored
114
-# if log_config_append is set. (list value)
115
-#default_log_levels = amqp=WARN,amqplib=WARN,boto=WARN,qpid=WARN,sqlalchemy=WARN,suds=INFO,oslo.messaging=INFO,oslo_messaging=INFO,iso8601=WARN,requests.packages.urllib3.connectionpool=WARN,urllib3.connectionpool=WARN,websocket=WARN,requests.packages.urllib3.util.retry=WARN,urllib3.util.retry=WARN,keystonemiddleware=WARN,routes.middleware=WARN,stevedore=WARN,taskflow=WARN,keystoneauth=WARN,oslo.cache=INFO,dogpile.core.dogpile=INFO
116
-
117
-# Enables or disables publication of error events. (boolean value)
118
-#publish_errors = false
119
-
120
-# The format for an instance that is passed with the log message. (string value)
121
-#instance_format = "[instance: %(uuid)s] "
122
-
123
-# The format for an instance UUID that is passed with the log message. (string
124
-# value)
125
-#instance_uuid_format = "[instance: %(uuid)s] "
126
-
127
-# Interval, number of seconds, of log rate limiting. (integer value)
128
-#rate_limit_interval = 0
129
-
130
-# Maximum number of logged messages per rate_limit_interval. (integer value)
131
-#rate_limit_burst = 0
132
-
133
-# Log level name used by rate limiting: CRITICAL, ERROR, INFO, WARNING, DEBUG or
134
-# empty string. Logs with level greater or equal to rate_limit_except_level are
135
-# not filtered. An empty string means that all levels are filtered. (string
136
-# value)
137
-#rate_limit_except_level = CRITICAL
138
-
139
-# Enables or disables fatal status of deprecations. (boolean value)
140
-#fatal_deprecations = false

+ 4
- 1
etc/armada/config-generator.conf View File

@@ -1,5 +1,8 @@
1 1
 [DEFAULT]
2 2
 output_file = etc/armada/armada.conf.sample
3
-wrap_width = 80
3
+wrap_width = 79
4 4
 namespace = armada.conf
5 5
 namespace = oslo.log
6
+namespace = oslo.policy
7
+namespace = oslo.middleware
8
+namespace = keystonemiddleware.auth_token

+ 5
- 0
etc/armada/policy-generator.conf View File

@@ -0,0 +1,5 @@
1
+[DEFAULT]
2
+output_file = etc/armada/policy.yaml.sample
3
+wrap_width = 79
4
+
5
+namespace = armada

examples/armada-manifest-v1.yaml → examples/keystone-manifest.yaml View File

@@ -22,7 +22,7 @@ metadata:
22 22
 data:
23 23
   chart_name: mariadb
24 24
   release: mariadb
25
-  namespace: undercloud
25
+  namespace: openstack
26 26
   timeout: 3600
27 27
   install:
28 28
     no_hooks: false
@@ -44,7 +44,7 @@ metadata:
44 44
 data:
45 45
   chart_name: memcached
46 46
   release: memcached
47
-  namespace: undercloud
47
+  namespace: openstack
48 48
   timeout: 100
49 49
   install:
50 50
     no_hooks: false
@@ -60,50 +60,6 @@ data:
60 60
     - helm-toolkit
61 61
 ---
62 62
 schema: armada/Chart/v1
63
-metadata:
64
-  schema: metadata/Document/v1
65
-  name: etcd
66
-data:
67
-  chart_name: etcd
68
-  release: etcd
69
-  namespace: undercloud
70
-  timeout: 3600
71
-  install:
72
-    no_hooks: false
73
-  upgrade:
74
-    no_hooks: false
75
-  values: {}
76
-  source:
77
-    type: git
78
-    location: git://github.com/openstack/openstack-helm
79
-    subpath: etcd
80
-    reference: master
81
-  dependencies:
82
-    - helm-toolkit
83
----
84
-schema: armada/Chart/v1
85
-metadata:
86
-  schema: metadata/Document/v1
87
-  name: rabbitmq
88
-data:
89
-  chart_name: rabbitmq
90
-  release: rabbitmq
91
-  namespace: undercloud
92
-  timeout: 100
93
-  install:
94
-    no_hooks: false
95
-  upgrade:
96
-    no_hooks: false
97
-  values: {}
98
-  source:
99
-    type: git
100
-    location: git://github.com/openstack/openstack-helm
101
-    subpath: rabbitmq
102
-    reference: master
103
-  dependencies:
104
-    - helm-toolkit
105
----
106
-schema: armada/Chart/v1
107 63
 metadata:
108 64
   schema: metadata/Document/v1
109 65
   name: keystone
@@ -111,8 +67,8 @@ data:
111 67
   chart_name: keystone
112 68
   test: true
113 69
   release: keystone
114
-  namespace: undercloud
115
-  timeout: 3600
70
+  namespace: openstack
71
+  timeout: 100
116 72
   install:
117 73
     no_hooks: false
118 74
   upgrade:
@@ -130,13 +86,12 @@ data:
130 86
 schema: armada/ChartGroup/v1
131 87
 metadata:
132 88
   schema: metadata/Document/v1
133
-  name: openstack-infra-services
89
+  name: keystone-infra-services
134 90
 data:
135
-  description: "OpenStack Infra Services"
91
+  description: "Keystone Infra Services"
136 92
   sequenced: True
137 93
   chart_group:
138 94
     - mariadb
139
-    - etcd
140 95
     - memcached
141 96
 ---
142 97
 schema: armada/ChartGroup/v1
@@ -157,5 +112,5 @@ metadata:
157 112
 data:
158 113
   release_prefix: armada
159 114
   chart_groups:
160
-    - openstack-infra-services
115
+    - keystone-infra-services
161 116
     - openstack-keystone

+ 15
- 4
requirements.txt View File

@@ -2,15 +2,17 @@ gitpython==2.1.5
2 2
 grpcio==1.6.0rc1
3 3
 grpcio-tools==1.6.0rc1
4 4
 keystoneauth1==2.21.0
5
+keystonemiddleware==4.9.1
5 6
 kubernetes>=1.0.0
6
-oslo.log==3.28.0
7
-oslo.messaging==5.28.0
8 7
 protobuf==3.2.0
9 8
 PyYAML==3.12
10 9
 requests==2.17.3
11 10
 sphinx_rtd_theme
12 11
 supermutes==0.2.5
13 12
 urllib3==1.21.1
13
+uwsgi>=2.0.15
14
+Paste>=2.0.3
15
+PasteDeploy>=1.5.2
14 16
 
15 17
 # API
16 18
 falcon==1.1.0
@@ -20,6 +22,15 @@ gunicorn==19.7.1
20 22
 cliff==2.7.0
21 23
 
22 24
 # Oslo
23
-oslo.log==3.28.0
24
-oslo.config>=3.22.0 # Apache-2.0
25
+oslo.cache>=1.5.0 # Apache-2.0
26
+oslo.concurrency>=3.8.0 # Apache-2.0
27
+oslo.config!=4.3.0,!=4.4.0,>=4.0.0 # Apache-2.0
28
+oslo.context>=2.14.0 # Apache-2.0
29
+oslo.messaging!=5.25.0,>=5.24.2 # Apache-2.0
30
+oslo.db>=4.24.0 # Apache-2.0
31
+oslo.i18n!=3.15.2,>=2.1.0 # Apache-2.0
32
+oslo.log>=3.22.0 # Apache-2.0
25 33
 oslo.middleware>=3.27.0 # Apache-2.0
34
+oslo.policy>=1.23.0 # Apache-2.0
35
+oslo.serialization!=2.19.1,>=1.10.0 # Apache-2.0
36
+oslo.utils>=3.20.0 # Apache-2.0

+ 2
- 0
setup.cfg View File

@@ -43,6 +43,8 @@ armada =
43 43
     test = armada.cli.test:TestServerCommand
44 44
 oslo.config.opts =
45 45
     armada.conf = armada.conf.opts:list_opts
46
+oslo.policy.policies =
47
+  armada = armada.common.policies:list_rules
46 48
 
47 49
 [pbr]
48 50
 warnerrors = True

+ 6
- 0
tools/keystone-account.sh View File

@@ -0,0 +1,6 @@
1
+#!/usr/bin/env bash
2
+
3
+openstack domain create 'ucp'
4
+openstack project create --domain 'ucp' 'service'
5
+openstack user create --domain ucp --project service --project-domain 'ucp' --password armada armada
6
+openstack role add --project-domain ucp --user-domain ucp --user armada --project service admin

+ 13
- 9
tox.ini View File

@@ -10,33 +10,37 @@ deps=
10 10
 setenv=
11 11
     VIRTUAL_ENV={envdir}
12 12
 usedevelop = True
13
+install_command = pip install {opts} {packages}
13 14
 commands =
14
-    find . -type f -name "*.pyc" -delete
15
-    python -V
16
-    py.test -vvv -s --ignore=hapi
15
+  find . -type f -name "*.pyc" -delete
16
+  python -V
17
+  py.test -vvv -s --ignore=hapi
17 18
 
18 19
 [testenv:docs]
19 20
 commands =
20
-    python setup.py build_sphinx
21
+  python setup.py build_sphinx
21 22
 
22 23
 [testenv:genconfig]
23 24
 commands =
24
-    oslo-config-generator --config-file=etc/armada/config-generator.conf
25
+  oslo-config-generator --config-file=etc/armada/config-generator.conf
26
+
27
+[testenv:genpolicy]
28
+commands =
29
+  oslopolicy-sample-generator --config-file=etc/armada/policy-generator.conf
25 30
 
26 31
 [testenv:pep8]
27 32
 commands =
28
-    flake8 {posargs}
33
+  flake8 {posargs}
29 34
 
30 35
 [testenv:bandit]
31 36
 commands =
32
-    bandit -r armada -x armada/tests -n 5
37
+  bandit -r armada -x armada/tests -n 5
33 38
 
34 39
 [testenv:coverage]
35 40
 commands =
36
-    nosetests -w armada/tests/unit --cover-package=armada --with-coverage --cover-tests --exclude=.tox
41
+  nosetests -w armada/tests/unit --cover-package=armada --with-coverage --cover-tests --exclude=.tox
37 42
 
38 43
 [flake8]
39 44
 filename= *.py
40 45
 ignore = W503,E302
41 46
 exclude= .git, .idea, .tox, *.egg-info, *.eggs, bin, dist, hapi
42
-

Loading…
Cancel
Save