update from RIFT as of 696b75d2fe9fb046261b08c616f1bcf6c0b54a9b second try
[osm/SO.git] / rwprojectmano / plugins / rwprojectmano / rift / tasklets / rwprojectmano / projectmano.py
1 #
2 # Copyright 2017 RIFT.IO Inc
3 #
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at
7 #
8 # http://www.apache.org/licenses/LICENSE-2.0
9 #
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15 #
16
17 """
18 Project Manager tasklet is responsible for managing the Projects
19 configurations required for Role Based Access Control feature.
20 """
21
22 import asyncio
23 import gi
24
25 gi.require_version('RwDts', '1.0')
26 gi.require_version('RwProjectManoYang', '1.0')
27 from gi.repository import (
28 RwDts as rwdts,
29 ProtobufC,
30 RwTypes,
31 RwProjectManoYang,
32 )
33 gi.require_version('RwKeyspec', '1.0')
34 from gi.repository.RwKeyspec import quoted_key
35
36 import rift.tasklets
37 from rift.mano.utils.project import (
38 NS_PROJECT,
39 get_add_delete_update_cfgs,
40 ProjectConfigCallbacks,
41 )
42
43
44 MANO_PROJECT_ROLES = [
45 { 'mano-role':"rw-project-mano:catalog-oper",
46 'description':("The catalog-oper Role has read permission to nsd-catalog "
47 "and vnfd-catalog under specific Projects, "
48 "as identified by /rw-project:project/rw-project:name. The "
49 "catatlog-oper Role may also have execute permission to specific "
50 "non-mutating RPCs. This Role is intended for read-only access to "
51 "catalogs under a specific project.") },
52
53 { 'mano-role':"rw-project-mano:catalog-admin",
54 'description':("The catalog-admin Role has full CRUDX permissions to vnfd and nsd "
55 "catalogs under specific Projects, as identified by "
56 "/rw-project:project/rw-project:name.") },
57
58 { 'mano-role':"rw-project-mano:lcm-oper",
59 'description':("The lcm-oper Role has read permission to the VL, VNF and NS "
60 "records within a Project. The lcm-oper Role may also have "
61 "execute permission to specific non-mutating RPCs.") },
62
63 { 'mano-role':"rw-project-mano:lcm-admin",
64 'description':("The lcm-admin Role has full CRUDX permissions to the VL, VNF "
65 "and NS records within a Project. The lcm-admin Role does "
66 "not provide general CRUDX permissions to the Project as a whole, "
67 "nor to the RIFT.ware platform in general.") },
68
69 { 'mano-role':"rw-project-mano:account-admin",
70 'description':("The account-admin Role has full CRUDX permissions to the VIM, SDN, VCA "
71 "and RO accounts within a Project. The account-admin Role does "
72 "not provide general CRUDX permissions to the Project as a whole, "
73 "nor to the RIFT.ware platform in general.") },
74
75 { 'mano-role':"rw-project-mano:account-oper",
76 'description':("The account-oper Role has read permission to the VIM, SDN, VCA "
77 "and RO accounts within a Project. The account-oper Role may also have "
78 "execute permission to specific non-mutating RPCs.") },
79 ]
80
81
82 class ProjectDtsHandler(object):
83 XPATH = "C,/{}".format(NS_PROJECT)
84
85 def __init__(self, dts, log, callbacks):
86 self._dts = dts
87 self._log = log
88 self._callbacks = callbacks
89
90 self.reg = None
91 self.projects = []
92
93 @property
94 def log(self):
95 return self._log
96
97 @property
98 def dts(self):
99 return self._dts
100
101 def get_reg_flags(self):
102 return (rwdts.Flag.SUBSCRIBER |
103 rwdts.Flag.DELTA_READY |
104 rwdts.Flag.CACHE |
105 rwdts.Flag.DATASTORE)
106
107 def add_project(self, cfg):
108 name = cfg.name
109 self._log.info("Adding project: {}".format(name))
110
111 if name not in self.projects:
112 self._callbacks.on_add_apply(name, cfg)
113 self.projects.append(name)
114 else:
115 self._log.error("Project already present: {}".
116 format(name))
117
118 def delete_project(self, name):
119 self._log.info("Deleting project: {}".format(name))
120 if name in self.projects:
121 self._callbacks.on_delete_apply(name)
122 self.projects.remove(name)
123 else:
124 self._log.error("Unrecognized project: {}".
125 format(name))
126
127 def update_project(self, cfg):
128 """ Update an existing project
129
130 Currently, we do not take any action on MANO for this,
131 so no callbacks are defined
132
133 Arguments:
134 msg - The project config message
135 """
136 name = cfg.name
137 self._log.info("Updating project: {}".format(name))
138 if name in self.projects:
139 pass
140 else:
141 self._log.error("Unrecognized project: {}".
142 format(name))
143
144 def register(self):
145 @asyncio.coroutine
146 def apply_config(dts, acg, xact, action, scratch):
147 self._log.debug("Got project apply config (xact: %s) (action: %s)", xact, action)
148
149 if xact.xact is None:
150 if action == rwdts.AppconfAction.INSTALL:
151 curr_cfg = self._reg.elements
152 for cfg in curr_cfg:
153 self._log.info("Project {} being re-added after restart.".
154 format(cfg.name))
155 self.add_project(cfg)
156 else:
157 self._log.debug("No xact handle. Skipping apply config")
158
159 return
160
161 add_cfgs, delete_cfgs, update_cfgs = get_add_delete_update_cfgs(
162 dts_member_reg=self._reg,
163 xact=xact,
164 key_name="name",
165 )
166
167 # Handle Deletes
168 for cfg in delete_cfgs:
169 self.delete_project(cfg.name)
170
171 # Handle Adds
172 for cfg in add_cfgs:
173 self.add_project(cfg)
174
175 # Handle Updates
176 for cfg in update_cfgs:
177 self.update_project(cfg)
178
179 return RwTypes.RwStatus.SUCCESS
180
181 @asyncio.coroutine
182 def on_prepare(dts, acg, xact, xact_info, ks_path, msg, scratch):
183 """ Prepare callback from DTS for Project """
184
185 action = xact_info.query_action
186 name = msg.name
187
188 self._log.debug("Project %s on_prepare config received (action: %s): %s",
189 name, xact_info.query_action, msg)
190
191 if action in [rwdts.QueryAction.CREATE, rwdts.QueryAction.UPDATE]:
192 if name in self.projects:
193 self._log.debug("Project {} already exists. Ignore request".
194 format(name))
195
196 else:
197 self._log.debug("Project {}: Invoking on_prepare add request".
198 format(name))
199 rc, err_msg = yield from self._callbacks.on_add_prepare(name, msg)
200 if rc is False:
201 xact_info.send_error_xpath(RwTypes.RwStatus.FAILURE,
202 ProjectDtsHandler.XPATH,
203 err_msg)
204 xact_info.respond_xpath(rwdts.XactRspCode.NACK)
205 return
206
207 elif action == rwdts.QueryAction.DELETE:
208 # Check if the entire project got deleted
209 fref = ProtobufC.FieldReference.alloc()
210 fref.goto_whole_message(msg.to_pbcm())
211 if fref.is_field_deleted():
212 if name in self.projects:
213 rc, delete_msg = yield from self._callbacks.on_delete_prepare(name)
214 if not rc:
215 self._log.error("Project {} should not be deleted. Reason : {}".
216 format(name, delete_msg))
217
218 xact_info.send_error_xpath(RwTypes.RwStatus.FAILURE,
219 ProjectDtsHandler.XPATH,
220 delete_msg)
221
222 xact_info.respond_xpath(rwdts.XactRspCode.NACK)
223 return
224 else:
225 self._log.warning("Delete on unknown project: {}".
226 format(name))
227
228 else:
229 self._log.error("Action (%s) NOT SUPPORTED", action)
230 xact_info.respond_xpath(rwdts.XactRspCode.NA)
231 return
232
233 xact_info.respond_xpath(rwdts.XactRspCode.ACK)
234
235 self._log.debug("Registering for project config using xpath: %s",
236 ProjectDtsHandler.XPATH)
237
238 acg_handler = rift.tasklets.AppConfGroup.Handler(
239 on_apply=apply_config,
240 )
241
242 with self._dts.appconf_group_create(acg_handler) as acg:
243 self._reg = acg.register(
244 xpath=ProjectDtsHandler.XPATH,
245 flags=self.get_reg_flags(),
246 on_prepare=on_prepare,
247 )
248
249
250 class ProjectHandler(object):
251 def __init__(self, tasklet, project_class):
252 self._tasklet = tasklet
253 self._log = tasklet.log
254 self._log_hdl = tasklet.log_hdl
255 self._dts = tasklet.dts
256 self._loop = tasklet.loop
257 self._class = project_class
258
259 self.mano_roles = [role['mano-role'] for role in MANO_PROJECT_ROLES]
260
261 self._log.debug("Creating project config handler")
262 self.project_cfg_handler = ProjectDtsHandler(
263 self._dts, self._log,
264 ProjectConfigCallbacks(
265 on_add_apply=self.on_project_added,
266 on_add_prepare=self.on_add_prepare,
267 on_delete_apply=self.on_project_deleted,
268 on_delete_prepare=self.on_delete_prepare,
269 )
270 )
271
272 def _get_tasklet_name(self):
273 return self._tasklet.tasklet_info.instance_name
274
275 def _get_project(self, name):
276 try:
277 proj = self._tasklet.projects[name]
278 except Exception as e:
279 self._log.exception("Project {} ({})not found for tasklet {}: {}".
280 format(name, list(self._tasklet.projects.keys()),
281 self._get_tasklet_name(), e))
282 raise e
283
284 return proj
285
286 def on_project_deleted(self, name):
287 self._log.debug("Project {} deleted".format(name))
288 try:
289 self._get_project(name).deregister()
290 except Exception as e:
291 self._log.exception("Project {} deregister for {} failed: {}".
292 format(name, self._get_tasklet_name(), e))
293
294 try:
295 proj = self._tasklet.projects.pop(name)
296 del proj
297 except Exception as e:
298 self._log.exception("Project {} delete for {} failed: {}".
299 format(name, self._get_tasklet_name(), e))
300
301 def on_project_added(self, name, cfg):
302 if name not in self._tasklet.projects:
303 try:
304 self._tasklet.projects[name] = \
305 self._class(name, self._tasklet)
306 self._loop.create_task(self._get_project(name).register())
307
308 except Exception as e:
309 self._log.exception("Project {} create for {} failed: {}".
310 format(name, self._get_tasklet_name(), e))
311 raise e
312
313 self._log.debug("Project {} added to tasklet {}".
314 format(name, self._get_tasklet_name()))
315 self._get_project(name)._apply = True
316
317 @asyncio.coroutine
318 def on_add_prepare(self, name, msg):
319 self._log.debug("Project {} to be added to {}".
320 format(name, self._get_tasklet_name()))
321
322 if name in self._tasklet.projects:
323 err_msg = ("Project already exists: {}".
324 format(name))
325 self._log.error(err_msg)
326 return False, err_msg
327
328 # Validate mano-roles, if present
329 try:
330 cfg = msg.project_config
331 users = cfg.user
332 for user in users:
333 for role in user.mano_role:
334 if role.role not in self.mano_roles:
335 err_msg = ("Invalid role {} for user {} in project {}".
336 format(role.role, user.user_name, name))
337 self._log.error(err_msg)
338 return False, err_msg
339
340 except AttributeError as e:
341 # If the user or mano role is not present, ignore
342 self._log.debug("Project {}: {}".format(name, e))
343
344 return True, ""
345
346 @asyncio.coroutine
347 def on_delete_prepare(self, name):
348 self._log.error("Project {} being deleted for tasklet {}".
349 format(name, self._get_tasklet_name()))
350 rc, delete_msg = yield from self._get_project(name).delete_prepare()
351 return (rc, delete_msg)
352
353 def register(self):
354 self.project_cfg_handler.register()
355
356
357 class ProjectStateRolePublisher(rift.tasklets.DtsConfigPublisher):
358
359 def __init__(self, tasklet):
360 super().__init__(tasklet)
361 self.proj_state = RwProjectManoYang.YangData_RwProject_Project_ProjectState()
362 self.projects = set()
363 self.roles = MANO_PROJECT_ROLES
364
365 def get_xpath(self):
366 return "D,/rw-project:project/rw-project:project-state/rw-project-mano:mano-role"
367
368 def get_reg_flags(self):
369 return super().get_reg_flags() | rwdts.Flag.DATASTORE
370
371 def role_xpath(self, project, role):
372 return "/rw-project:project[rw-project:name={}]".format(quoted_key(project)) + \
373 "/rw-project:project-state/rw-project-mano:mano-role" + \
374 "[rw-project-mano:role={}]".format(quoted_key(role['mano-role']))
375
376 def pb_role(self, role):
377 pbRole = self.proj_state.create_mano_role()
378 pbRole.role = role['mano-role']
379 pbRole.description = role['description']
380 return pbRole
381
382 def publish_roles(self, project):
383 if not project in self.projects:
384 self.projects.add(project)
385 for role in self.roles:
386 xpath = self.role_xpath(project, role)
387 pb_role = self.pb_role(role)
388 self.log.debug("publishing xpath:{}".format(xpath))
389 self._regh.update_element(xpath, pb_role)
390
391 def unpublish_roles(self, project):
392 if project in self.projects:
393 self.projects.remove(project)
394 for role in self.roles:
395 xpath = self.role_xpath(project, role)
396 self.log.debug("unpublishing xpath:{}".format(xpath))
397 self._regh.delete_element(xpath)
398